Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions docs/developer/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,47 @@ failed renderer build leaves the displayed renderer and last published assets
untouched, and the next valid edit retries normally.

An edit used by Electron's main process or local backend prints the message
`restart required` and leaves the running window untouched. Stop and restart the loop
when that change should enter the application; the loop never turns a runtime
edit into an automatic app restart. Watcher implementation updates hand the
running app to the replacement watcher without quitting it. Generated output,
dependency directories, Git metadata, and instance state do not trigger
`restart required` and leaves the running window untouched. Stop and restart
the loop when that change should enter the application; the loop never turns a
runtime edit into an automatic app restart. Watcher implementation updates hand
the running app to the replacement watcher without quitting it. Generated
output, dependency directories, Git metadata, and instance state do not trigger
rebuilds. Keep only one loop per instance and use `npm start` for deterministic
one-shot work. End the loop with Ctrl-C; it asks the addressed instance to quit
through the managed durability path and waits for that process before
returning.

### Shared development element callouts

While the live development loop is running, Option-click a rendered element
whose path from its nearest unique ID is at most 128 elements. Markover pins a
bright bounding box to that element and copies one opaque `mko-ui-v1:`
reference to the clipboard. Deeper targets report that they cannot be
referenced and copy nothing. Paste a copied reference into the agent thread; no
screenshot, DevTools inspection, or hand-drawn circle is needed.

An agent highlights the same element in the addressed running instance with:

```sh
npm --silent run markover -- --instance dev element highlight '<mko-ui-v1-reference>'
```

Clear the pinned box with:

```sh
npm --silent run markover -- --instance dev element clear
```

Omit `--instance dev`, or pass `--instance canonical`, when `npm run dev`
addresses the canonical checkout instead of a pull-request worktree. Element
commands address only an already-running watcher and never cold-start Markover.

References use a validated unique-ID anchor and deterministic same-tag child
indices, sibling counts, and structural fingerprints.
Stale or ambiguous references fail instead of selecting a different element.
The picker and authenticated highlight route exist only in a running live
development watcher; release and non-watch instances do not expose them.

## Development review links

Install or inspect the forwarding-only handler for the current worktree's open
Expand Down
1 change: 1 addition & 0 deletions scripts/app-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const runtimeModuleNames = [
'codex-thread-titles',
'development-control',
'development-config',
'development-element',
'durability-shutdown',
'ipc-contract',
'ipc-security',
Expand Down
60 changes: 58 additions & 2 deletions scripts/markover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { serviceEndpointPath } from '../src/service-endpoint'
import { guidance } from '../src/agent-guidance'
import { normalizeSettings } from '../src/settings'
import { isDevelopmentElementCalloutResult } from '../src/development-element'
import { parseMarkdown } from '../src/tree'
import {
cleanupDevelopmentInstance,
Expand Down Expand Up @@ -110,6 +111,8 @@ export type ParsedCommand = ParsedInstanceTarget & (
| { command: 'canonical'; action: 'doctor' }
| { command: 'canonical'; action: 'refresh'; install: boolean }
| { command: 'cleanup'; expectedIdentity: `pr-${number}` }
| { command: 'element'; action: 'clear' }
| { command: 'element'; action: 'highlight'; reference: string }
| { command: 'edit'; reviewId: string }
| {
command: 'resolve'
Expand Down Expand Up @@ -309,6 +312,11 @@ export function helpPayload() {
usage: '--instance dev cleanup <pr-N>',
purpose: 'Move one stopped worktree-local instance to macOS Trash after its development URL handler has been removed.'
},
{
name: 'element',
usage: '[--instance <canonical|dev>] element highlight <mko-ui-v1-reference> | [--instance <canonical|dev>] element clear',
purpose: 'Highlight or clear one exact element callout in the addressed running live development window.'
},
{
name: 'help',
aliases: ['info', '--help', '-h'],
Expand Down Expand Up @@ -398,7 +406,8 @@ export function parseCommandArguments(args: string[]): ParsedCommand {
command !== 'pending' &&
command !== 'resolve' &&
command !== 'unresolve' &&
command !== 'cleanup'
command !== 'cleanup' &&
command !== 'element'
) {
if (command === 'check') {
throw commandError(
Expand All @@ -414,7 +423,28 @@ export function parseCommandArguments(args: string[]): ParsedCommand {
}
throw commandError(
`Unknown command: ${command}`,
'markover <open|get|get-for-review|submit|revise|done|edit|pending|resolve|unresolve|canonical|cleanup|help> ...'
'markover <open|get|get-for-review|submit|revise|done|edit|pending|resolve|unresolve|canonical|cleanup|element|help> ...'
)
}

if (command === 'element') {
if (rest.length === 1 && rest[0] === 'clear') {
return targeted({ command, action: 'clear' as const })
}
if (
rest.length === 2 &&
rest[0] === 'highlight' &&
rest[1]?.startsWith('mko-ui-v1:')
) {
return targeted({
command,
action: 'highlight' as const,
reference: rest[1]
})
}
throw commandError(
'element requires highlight with one copied reference, or clear.',
'markover [--instance <canonical|dev>] element highlight <mko-ui-v1-reference> | markover [--instance <canonical|dev>] element clear'
)
}

Expand Down Expand Up @@ -1152,6 +1182,7 @@ export async function ensureService({

export interface ExecuteCommandOptions {
endpointPath?: string
requestLocal?: typeof requestJson
ensure?: () => Promise<void>
resolveTarget?: (
selector: InstanceSelector,
Expand Down Expand Up @@ -1622,6 +1653,31 @@ export async function executeCommand(
const cleanup = options.cleanup || cleanupDevelopmentInstance
return cleanup(instance, parsed.expectedIdentity)
}
if (parsed.command === 'element') {
const endpointPath = options.endpointPath || (
await resolveTarget(selector)
).service.endpointPath
const result = await (options.requestLocal || requestJson)(
endpointPath,
'POST',
'/development/element-callout',
parsed.action === 'clear'
? { action: 'clear' }
: { action: 'highlight', reference: parsed.reference }
)
if (!isDevelopmentElementCalloutResult(result)) {
throw new LocalServiceError(
'INVALID_RESPONSE',
'Markover returned an invalid development element callout response.',
200
)
}
return {
...(result.bounds ? { bounds: result.bounds } : {}),
...(result.reference ? { reference: result.reference } : {}),
status: result.status
}
}

const profile = parsed.instance === undefined
? await (options.loadRemoteProfile || loadRemoteProfile)()
Expand Down
9 changes: 9 additions & 0 deletions src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import type {
StartupPhaseEvent,
StartupReady
} from './startup-contract'
import type {
DevelopmentElementCalloutCommand,
DevelopmentElementCalloutResult
} from './development-element'

export interface DiffStats {
additions: number
Expand Down Expand Up @@ -754,6 +758,11 @@ declare global {
createLocalReview: (tree: ReviewTree) => Promise<MarkoverDocument>
onOpenMarkdownRequested: (callback: () => void) => void
onReviewBatchModeRequested: (callback: () => void) => void
onDevelopmentElementCallout: (
callback: (
command: DevelopmentElementCalloutCommand
) => DevelopmentElementCalloutResult | Promise<DevelopmentElementCalloutResult>
) => void
checksum: (source: string) => Promise<string>
copyText: (text: string) => void
readClipboardImage: () => Promise<MarkoverClipboardImage | null>
Expand Down
Loading