@leanandmean/coding-agent 0.74.1-scramjet.6 → 0.74.1-scramjet.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/LICENSE +202 -0
- package/README.md +21 -9
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/initial-message.d.ts +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +3 -0
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/list-models.d.ts +2 -0
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +4 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -22
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +12 -3
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +64 -38
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +86 -16
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +865 -181
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +21 -17
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -5
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +43 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/exec.d.ts +12 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +74 -28
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.js +4 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +9 -5
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +162 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +110 -6
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +179 -28
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-throughput-history.d.ts +25 -0
- package/dist/core/output-throughput-history.d.ts.map +1 -0
- package/dist/core/output-throughput-history.js +240 -0
- package/dist/core/output-throughput-history.js.map +1 -0
- package/dist/core/output-throughput.d.ts +43 -0
- package/dist/core/output-throughput.d.ts.map +1 -0
- package/dist/core/output-throughput.js +161 -0
- package/dist/core/output-throughput.js.map +1 -0
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +76 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scramjet-command-parser.d.ts +4 -0
- package/dist/core/scramjet-command-parser.d.ts.map +1 -1
- package/dist/core/scramjet-command-parser.js +72 -1
- package/dist/core/scramjet-command-parser.js.map +1 -1
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +61 -10
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +46 -11
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +18 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +84 -16
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +6 -2
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +16 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +38 -10
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts +4 -2
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +14 -8
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +19 -8
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +14 -6
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +5 -3
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +4 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +22 -18
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +9 -3
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +87 -16
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +129 -36
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.js +2 -2
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +37 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +901 -176
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/spellcheck.d.ts +26 -0
- package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
- package/dist/modes/interactive/spellcheck.js +144 -0
- package/dist/modes/interactive/spellcheck.js.map +1 -0
- package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
- package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
- package/dist/modes/interactive/theme/theme-schema.json +6 -1
- package/dist/modes/interactive/theme/theme.d.ts +24 -4
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +123 -22
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +5 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +6 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +33 -14
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +278 -10
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +7 -6
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +17 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +88 -21
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +9 -3
- package/docs/custom-provider.md +13 -4
- package/docs/extensions.md +139 -21
- package/docs/keybindings.md +25 -1
- package/docs/models.md +34 -2
- package/docs/packages.md +2 -0
- package/docs/prompt-templates.md +2 -0
- package/docs/quickstart.md +1 -4
- package/docs/rpc.md +8 -6
- package/docs/sdk.md +22 -3
- package/docs/session-format.md +2 -1
- package/docs/settings.md +13 -3
- package/docs/skills.md +7 -5
- package/docs/terminal-setup.md +79 -2
- package/docs/terminal-verification.md +71 -0
- package/docs/themes.md +63 -10
- package/docs/tui.md +120 -25
- package/docs/usage.md +6 -4
- package/examples/extensions/border-status-editor.ts +2 -3
- package/examples/extensions/doom-overlay/doom-component.ts +3 -3
- package/examples/extensions/mac-system-theme.ts +3 -2
- package/examples/extensions/modal-editor.ts +13 -8
- package/examples/extensions/overlay-qa-tests.ts +22 -9
- package/examples/extensions/overlay-test.ts +5 -3
- package/examples/extensions/snake.ts +3 -1
- package/examples/extensions/space-invaders.ts +6 -3
- package/examples/extensions/subagent/index.ts +20 -17
- package/package.json +69 -58
package/docs/tui.md
CHANGED
|
@@ -6,6 +6,60 @@ Extensions and custom tools can render custom TUI components for interactive use
|
|
|
6
6
|
|
|
7
7
|
**Source:** [`@leanandmean/tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)
|
|
8
8
|
|
|
9
|
+
## Transcript Rendering
|
|
10
|
+
|
|
11
|
+
**Scramjet divergence source:** [`packages/tui/src/tui.ts`](../../tui/src/tui.ts)
|
|
12
|
+
|
|
13
|
+
Interactive mode uses an application-managed retained viewport in the alternate screen. The rightmost scrollbar owns browsing of the current transcript, including output that is still changing. Native terminal scrollback is not the active transcript. Streaming and completed tools keep their ordinary grouped presentation; finalization changes cache eligibility without appending a duplicate card.
|
|
14
|
+
|
|
15
|
+
The logical document keeps the existing component ownership and order. By default its above-editor widgets, current editor-slot occupant, below-editor widgets and footer form a bottom dock; header, chat, queue and working status remain scrollable. Live `/settings` controls docking, maximum input-text height and wheel step. The percentage is a text-row ceiling, not reserved space or a selector-height limit; the editor fits around the other dock occupants before the renderer suspends docking with a visible explanation. Oversized content remains in the retained scrolling flow. Tool-attached approval context and controls stay together in the transcript, with dock space reclaimed while they own input. Undocking preserves component identity and does not switch renderer.
|
|
16
|
+
|
|
17
|
+
Passive updates, docked editing and configured presentation toggles preserve the reading position. Undocked editing reveals its cursor. Overlays remain screen-relative. Session reconstruction resets browsing state, but ordinary updates, expansion, styling, docking and resize attempt content-relative anchoring.
|
|
18
|
+
|
|
19
|
+
See [terminal-setup.md](terminal-setup.md#transcript-browsing-and-copying) for interaction and terminal-configuration requirements. Session data, tool result contracts, RPC/print output and standalone HTML export are unchanged; viewport state is not serialized.
|
|
20
|
+
|
|
21
|
+
### Retained viewport API
|
|
22
|
+
|
|
23
|
+
`TUI.configureViewport({ getBlocks, keybindings?, copy?, requestPaste?, getScrollWheelStep?, handlePresentationInput?, keepReadingOnInput?, allowViewportKeys?, minimumSize?, handleBlockedInput? })` selects the retained rendering path. InteractiveMode configures it at startup using the mounted production components; standalone TUI callers opt in explicitly. Only promoted committed components can skip producer rendering; completed tools behind a pending predecessor remain live and browseable. Live producers still render, but their normalized rows and copy spans can be reused when raw output, copy provenance and geometry remain unchanged. Promotion preserves component identity. Thinking visibility and expansion update existing components; session/tree/reload reconstruction resets browsing and selection. `getBlocks()` returns the ordered projection of existing component instances as `{ component, finalized?, revision?, dock?, fitHeight?, renderDockGap? }`. Each component must occur once, and dock-tagged blocks must form a suffix. Fixed dock occupants are measured before `fitHeight(rows)` assigns a remaining row ceiling to the input slot; an oversized band uses the scrolling flow instead. The callback does not authorize clipping arbitrary text. `EditorComponent.setHeightLimit?(limits)` is a separate optional input-sizing hook: `limits()` returns `{ rows, text }`, the current total editor budget and wrapped-text ceiling. Built-in editors use it for cursor-following and page movement and budget autocomplete separately. Optional `EditorComponent.isShowingAutocomplete()` exposes an active completion menu even when it hides the hardware cursor. InteractiveMode uses this capability to reveal a hidden input slot and gate completion acceptance until its current visible paint has flushed, without taking input from capturing overlays. Completion acceptance also checks the text/cursor snapshot that produced the displayed menu, independently of viewport visibility and flush evidence. If that snapshot is stale, Tab requests fresh completion; Enter follows normal handling of the current draft rather than applying the stale prefix, and confirmation-only remaps cannot fall through to unrelated editor actions. Custom editors must cooperate to honor the input-height preference; non-fitting custom components are not silently clipped vertically. The existing `Component.setViewportHeight` contract remains image-only. Finalization changes cache eligibility, not identity or transcript ownership. Increment `revision` when a finalized block changes; `tui.invalidate()` invalidates projected components and finalization-based reuse. Width changes and `rebuild()` refresh rendering without resetting the reader's anchor; unchanged live output may still reuse its verified normalization. Call `resetViewport()` only for genuine content/session replacement.
|
|
24
|
+
|
|
25
|
+
An optional `renderDockGap(width, rowsBelow)` on the first dock block presents one text row over that block's existing empty leading row, only while the dock fits. It runs during slicing after final layout and pending revelation, not during producer rendering. The row must contain only width-contained text and SGR styling; invalid placement/content is an error. Image-covered rows and image placeholders are not replaced. Logical rows, anchors, pointer targets and painted-copy sources remain unchanged, so this chrome is not selection-highlighted or copied. InteractiveMode uses its existing above-widget spacer for the labelled Session continuation count, leaving that row blank at the tail and omitting the indicator when undocked or suspended.
|
|
26
|
+
|
|
27
|
+
The viewport retains all logical rows, renders components at terminal width minus one, and paints a bounded slice using absolute screen coordinates. The reserved last column displays a scrollbar; at a one-column terminal there is no scrollbar. `scrollViewport(delta)` and `scrollViewportTo(offset, anchorScreenRow = 0)` provide programmatic navigation. `followViewport()` releases held selection and resumes tail-following; InteractiveMode invokes it for nonempty editor submission before routing or queueing (both Enter and Alt+Enter), and when a new user message appears, including queued messages when delivered. Scrolling to the bottom resumes tail-following; passive output updates and resize clamping do not. `getViewportState()` returns the scrollable transcript's offset, total rows, visible height, and tail-following state, excluding a fitted dock; selection does not reserve a row. `refreshViewportLayout()` clears held selection/gestures and invalidates layout without resetting the reader's anchor; use it for explicit layout-setting changes. Overlays remain screen-relative, and cursor/IME positioning uses the visible slice rather than the document tail.
|
|
28
|
+
|
|
29
|
+
Ordinary image-free frames without held selection assemble visible transcript and dock ranges directly from cached blocks, rather than flattening every historical row. Cursor revelation uses cached per-block marker positions. Image/cursor summaries follow the same normalized-row invalidation as the block cache. This does not virtualize producers: block traversal and necessary live rendering/provenance checks remain, and changed blocks are still validated completely, including offscreen rows. Image-bearing presentations retain the full-document graphics path because an image's payload row need not be its placement top. Active selection uses the full-document slicing path. Each paint records its immutable row/provenance references and endpoint values; live producers and geometry continue updating. All logical rows remain retained and reachable, with no history cap.
|
|
30
|
+
|
|
31
|
+
Anchors use ordered row correspondence with a bounded raw-row search, then visible grapheme correspondence across reflow and restyling. Exhausting the raw-row search still attempts content correspondence; it is not evidence of deletion. If whole-block searches exhaust their edit budgets, a unique surviving row among at most 64 rows from the anchor can bound one additional ordered prefix search. This excludes distant growth without choosing the first repeated anchor string; ambiguous boundaries or an exhausted prefix budget retain the existing fallback. Comparison ignores ANSI, whitespace, and wrap boundaries without altering displayed rows or their spacing. The anchored grapheme retains its chosen screen-row offset where geometry permits. Deletion falls back to surviving content in the same block, then the nearest surviving adjacent block (following block wins ties). Blank-only blocks preserve a clamped row ordinal; arbitrary width-dependent or repeated/whitespace-only content cannot promise exact semantic source identity.
|
|
32
|
+
|
|
33
|
+
`renderNow({ requireFlush: true })` renders immediately and rejects if the terminal cannot flush or flushing fails. It waits for that invocation's output, not necessarily a newer frame rendered during the wait. `isViewportFrameFlushed()` checks whether the current complete viewport paint, including overlays, has a matching successful flush; an older flush cannot certify a newer presentation. Scheduled and immediate paints share this settlement path. Failed scheduled flushes leave controls gated and produce a bounded diagnostic; immediate callers still receive the original rejection. Stop, reset and rebuild revoke previous paint evidence. It is independent of `commitNow()`, whose committed-history preconditions and flush guarantee remain unchanged. Viewport and committed live-region configuration are mutually exclusive.
|
|
34
|
+
|
|
35
|
+
Retained ordinary text normalizes terminal presentation and expands tabs to three spaces before width containment. Overwide component rows are clipped with a bounded diagnostic rather than crashing the session; caching, selection and copying use the same contained rows. Source messages, tool results and HTML export remain unchanged, and graphics retain separate atomic placement handling. Components must still obey the width contract. `isComponentRenderComplete(component)` checks the entire projected block, including offscreen rows, and returns false for clipping, unavailable or invalidated presentation, or a held selection. Completeness is separate from visible control geometry and flush settlement.
|
|
36
|
+
|
|
37
|
+
The viewport owns transcript wheel scrolling, track clicks and thumb dragging. `getScrollWheelStep()` supplies the application's validated step. Thumb mapping stays fixed during a gesture, then reconciles with current bounds; releasing at the gesture's bottom resumes following the current tail after growth. Configurable `tui.viewport.pageUp/pageDown` default to Alt+PageUp/Alt+PageDown and allow entry from the tail; `allowViewportKeys(data)` lets the application preserve conflicting selector bindings. PageUp/PageDown/Escape retain their fixed detached-only browsing behavior when the input owner permits it. Ctrl+Home/Ctrl+End navigate to the transcript beginning/bottom at every scroll position; Home/End retain editor line-start/end behavior. Selector bindings, including cancellation, take precedence. `keepReadingOnInput()` preserves position for input in a fitted dock, while `handlePresentationInput(data)` optionally dispatches application-owned presentation toggles without reattaching. These callbacks do not supersede overlay or selection-copy precedence. Ordinary undocked input reveals its cursor marker on the next frame, even with tall trailing widgets. Later viewport-consumed input or programmatic navigation cancels pending cursor and component revelation, including hidden-completion revelation. Visible overlays cancel underlying selection and gestures. Raw-input listeners registered before configuration still run first; do not transform or consume pointer packets if the viewport must own them. Interactive extension listeners are registered after viewport configuration and do not receive consumed pointer/browsing events. Kitty-capable viewport terminals request flags 15 (including explicit escape encoding for all keys), so raw listeners must use the key parser and ignore release events rather than assuming Enter is a bare carriage return. This prevents older Kitty versions' legacy Enter release bytes from looking like a second activation. Pure modifier presses are consumed by the viewport without moving the reader or clearing selection, so forming a copy chord preserves its target. Keyboard stacks are restored in their owning screen buffer; unconfigured terminals retain flags 7.
|
|
38
|
+
|
|
39
|
+
Ordinary drag selection snaps to displayed grapheme boundaries and copies selected displayed content without ANSI controls or scrollbar cells. Stationary left press/release pairs at the same cell and unchanged document point/row advance through single, double and triple clicks, then restart; successive presses must be within 500 ms. Double-click selects a contiguous word, punctuation or whitespace run using the existing editor character classes. Triple-click selects one physical displayed row, without extending through soft wraps or adding a newline. Copy provenance still excludes layout padding and preserves genuine whitespace. Actual drag motion, navigation, ordinary keys, other buttons, focus loss, visible overlays, resize, reset and stop break click counting; paints alone do not. Dragging after multi-click uses the existing character-endpoint behavior, not word/line-wise extension. Built-in rendering supplies optional copy provenance to exclude layout padding and rejoin soft wraps without guessing at source indentation or hard breaks. Selection can cross the transcript/dock seam in either direction. Downward transcript dragging first autoscrolls to the tail before extending into the dock, including with a stationary pointer. Dock-origin upward dragging captures the visible transcript boundary, excluding the hidden gap below it. Cross-seam selection retains a visual boundary rather than extending that boundary through newly inserted offscreen rows. Transcript selection supports edge autoscroll and, after an initial drag, wheel-driven extension while the left button stays held, including with a stationary pointer. Initial press and pre-scroll motion target the painted rows. Screen-only filler below a short transcript cannot start selection; genuine blank content rows remain selectable. Once wheel or edge scrolling begins, motion and wheel endpoints use the resulting offset and height even when events interleave before repaint; wheel coordinates also refresh the edge timer's pointer position. Horizontal adjustments keep displayed grapheme boundaries. A press without a drag does not start wheel selection; releasing the button stops pointer-driven extension. Wheel browsing does not extend a selection confined to the dock. Selection follows surviving component-local rows and columns on a best-effort basis during replacement, deletion and reflow; layout and waiting controls remain live. Copy captures the last painted highlight and its copy provenance synchronously before invoking the asynchronous clipboard backend. Pending pointer movement or producer updates cannot alter that captured text. A new, not-yet-painted selection consumes Copy without copying or falling through to editor clearing. Copy failure temporarily covers the last screen row with an actionable diagnostic, retaining selection and geometry; that painted diagnostic cannot select the underlying text, even before its dismissal repaints. Successful copy, clearing input, reset or stop clears selection; usable resize ends the gesture and remaps selection. Successful copy and empty-click release restore prior following intent unless explicit navigation moved away; passive output growth does not discard that intent. Generic cancellation does not restore following. Right-click with a nonempty selection and the injected manager's `tui.input.copy` action invoke `copy(text): Promise<void>`; absent selection, Ctrl+C keeps its normal behavior. Copy failures are displayed and retain the selection. Callback resolution means the backend accepted the request, not that the desktop clipboard was independently verified; OSC 52 has no such acknowledgement. Without selection, right-click in any content column invokes optional `requestPaste()` without a pointer-target component. The callback's application owns paste eligibility; the viewport suppresses requests for overlays, the scrollbar, and copying in progress. InteractiveMode targets only its actual visible, render-complete focused editor regardless of pointer row, never selectors or approval controls. It captures input/edit/slot/session currentness before awaiting any pending presentation flush, revalidates before reading the local clipboard with bounded platform commands and before insertion, strips terminal framing/control bytes, and inserts through bracketed paste without submission. Focus-in, cell-size notifications and unmodified right-button motion do not invalidate a pending paste; real input and focus loss do. Eligible retries share an outstanding read with independent request snapshots, so only the latest still-current request inserts text; unrelated input still invalidates pending requests. On WSL, InteractiveMode owns a prewarmed PowerShell reader using direct Windows Unicode clipboard access, with no background clipboard reads or cached text. Startup and reads are separately bounded to five seconds; decoded text is capped at 10 MiB, framing is validated, and brief clipboard-lock acquisition is retried at most ten times before any data is read. Timeout, malformed output and process/pipe failure retire the worker without repeating the read; a later explicit paste may start a new worker. Readiness is revalidated against input ownership before dispatch. Clipboard copy remains single-flight even if its originating selection is replaced; stale settlement cannot clear a replacement selection. Emulator-owned menus cannot see application selection, and native clipboard/interaction checks remain necessary for each supported path.
|
|
40
|
+
|
|
41
|
+
Copy provenance uses the unchanged `Component.render(width): string[]` contract. `setRenderedCopy(lines, rows)` validates and associates an immutable metadata snapshot with that exact returned array; equivalent publication on unchanged output reuses the verified snapshot. `getRenderedCopy(lines)` returns it only while the output still matches its witness, otherwise falling back to physical-cell copying. Component caches verify both output and copy provenance, including metadata-only changes and externally rebound output. Each `RenderedCopyRow` is either `null` for layout padding or `{ start, end, after? }`, using half-open display-column bounds. `after` records only the whitespace replaced by a soft wrap (`""` for a midword split); absent `after` means a hard line boundary. Metadata never supplies alternate non-whitespace content. Text, Markdown, Box and Container propagate it through wrapping and padding; known zero-width OSC wrappers reattach it after their transformation. Cached and painted viewport rows retain their own copy snapshot, independent of later producer updates. The editor annotates visible draft chunks using logical-line boundaries, excluding borders, padding and the synthetic cursor space; hidden draft rows are not recovered. Custom wrappers that mutate or reconstruct arrays must deliberately propagate correct metadata or retain the conservative fallback. Tables and repeated non-whitespace quote prefixes keep their physical structure, and pre-render trimming/collapsing is not reversed. Copying a padding-only held range does not fall through to editor clearing.
|
|
42
|
+
|
|
43
|
+
`ProcessTerminal` implements the additive `Terminal.setViewportMode(enabled)` capability, required and checked before viewport configuration. Injected InteractiveMode terminals must implement this capability; there is no silent fallback to the clipping renderer. TUI start/stop coordinates idempotent alternate-screen, SGR button-motion and focus-reporting mode entry/restoration; unconfigured callers never enable these modes. Focus packets are consumed before ordinary input. Focus-out cancels unfinished selection/scrollbar gestures without returning to the tail, preserves completed selections, and stops edge autoscroll. Recognized releases terminate gestures even outside screen bounds; boundary scrolling does not keep repainting unchanged rows. Focus-in does not revive an abandoned gesture. These guarantees depend on the terminal delivering focus reports. Recognized incomplete mouse packets are quarantined after the transport timeout and discarded through a terminator or next escape, rather than leaking their tails into editor text. While mouse reporting is enabled, `StdinBuffer.setMouseReporting(true)` also retains one recognition credit after emitting an ambiguous Escape/CSI prefix. A subsequent complete SGR-shaped suffix is discarded even after a long idle gap; it does not replay the Escape or trigger a late pointer action. Nonmatching new bytes are replayed literally, and incomplete speculative suffixes are released after the framing timeout (10 ms in ProcessTerminal). Identical independently typed mouse-shaped text is inherently ambiguous and is consumed in this one-credit position; a suffix fragmented beyond the timeout can still become literal input. Unrelated input, paste, mode exit and reset clear the credit; recognized `ESC[<` quarantine remains separate. Standalone callers without mouse reporting retain the shorter prefix-recovery window. Headless `sendInput` bypasses transport framing and cannot establish fragmentation safety.
|
|
44
|
+
|
|
45
|
+
In retained mode, tool-attached context uses `renderNow({ requireFlush: true })` after installing its full retained context and revealing the pending tool. Controls receive focus only after a successful required flush matches the current presentation and both the full context and controls are render-complete. Missing/failed flushing, replaced context, clipped context (even offscreen), or controls that cannot fit wholly in the viewport fail closed. If a newer presentation supersedes a pending flush, controls remain unfocused until a later reveal/flush establishes current evidence; the older completion cannot authorize the newer frame. While browsing away, the first non-browsing input reveals and flushes controls without activating them; only subsequent input can authorize. `isComponentVisible(component)` checks the last painted, fully visible projected block (not a scheduled scroll); `revealComponent(component)` requests its revelation on the next frame. `getComponentVisibility(component)` distinguishes `visible`, `occluded` (a current or last-painted overlay), and `outside` (non-fit, stale geometry or stopped rendering). Temporary overlay occlusion does not cancel approval: after dismissal, controls require fresh revelation and flushing before a later activation. These visibility methods concern projected blocks, not arbitrary nested children. `isComponentFocused(component)` identifies the actual input owner so approval guards do not steal input from a capturing overlay. `replaceFocus(expected, next)` conditionally updates both actual focus and saved overlay restoration references matching `expected`, preserving other focused overlays and newer targets across asynchronous settlement/cancellation.
|
|
46
|
+
|
|
47
|
+
`ui.stop()` restores the normal buffer and input modes without printing transcript copies. Interactive suspension and both external-editor paths first await the existing bounded input drain, so the opening key's release does not leak to the shell or editor. Late keyboard-query responses cannot re-enable reporting during that drain. Restart restores the retained view independently of the asynchronous keyboard-protocol query. `TUI.addLifecycleListener(listener)` returns an unsubscribe function and synchronously reports each actual `"start"`/`"stop"` transition, without replaying the current state; listeners must not throw. InteractiveMode uses this to prewarm and close its WSL clipboard reader across all handoffs, revoking pending paste requests on stop. A process-exit hook also terminates an active worker.
|
|
48
|
+
|
|
49
|
+
`TUI.stop({ retainContent: true })` optionally refreshes and appends the retained viewport's complete normalized document after restoring the normal buffer. It preserves row order, width, SGR styling and the rendered dock suffix, removes cursor markers, resets line styles/hyperlinks, and replaces images with labelled placeholders. It excludes selection/overlay/scrollbar decorations. Snapshot failure still restores terminal modes and then throws; callers must not retry an ambiguous output write. No-argument stop remains no-dump, repeated stop is idempotent, and legacy/committed modes do not duplicate their native history. InteractiveMode forwards this option only from orderly shutdown when the live `retainTranscriptOnExit` setting is enabled (default `false`), before disposing presentation dependencies. Retention uses the current component projection, not session JSON or a second transcript renderer; hidden draft rows, collapsed details and earlier compacted/other-branch content are not reconstructed. Session saving and explicit export remain unchanged. Orderly shutdown awaits terminal flushing before runtime disposal; crash cleanup restores modes without requesting retention, and terminal-loss cleanup deliberately writes nothing.
|
|
50
|
+
|
|
51
|
+
Built-in Kitty/iTerm2 placements are re-anchored to absolute rows only when their full extent fits; clipped spans show a placeholder and remain reachable by scrolling. Repeated reports of unchanged cell dimensions are consumed without invalidating components or disturbing image reading anchors; genuine dimension changes still invalidate and re-render images. `Component.setViewportHeight?(height)` is an optional image-sizing bound, propagated by standard Container/Box rendering, including newly added children. Custom wrappers must forward it to nested images. It must not truncate ordinary text or bound the total stacked document. Built-in Image fits using existing aspect-preserving sizing without changing source data; finalized viewport caches refresh on height changes. iTerm2 placements specify both cell dimensions to avoid rounded width exceeding reserved height. Transcript images are withheld with visible placeholders during overlays and selection; clearing either restores eligible placements. Bounded overlays forward their resolved image-height limit before rendering and use the same atomic geometry validation before clipping, including nested/padded images. A higher overlapping overlay suppresses the entire lower image placement; clearing it restores eligible placements. Custom graphics envelopes without recognizable placement geometry cannot be safely clipped and receive a placeholder. Built-in graphics checks do not establish arbitrary custom-envelope compatibility. See [terminal-setup.md](terminal-setup.md#native-compatibility-evidence) for the bounded native evidence.
|
|
52
|
+
|
|
53
|
+
### Minimum usable geometry
|
|
54
|
+
|
|
55
|
+
InteractiveMode opts into `minimumSize: { columns: 12, rows: 3 }`, measured in physical terminal cells. Below that threshold the retained renderer shows a bounded resize notice, suppresses overlays/images/cursors and ordinary input, and does not render components into a destructive temporary budget. Draft and autocomplete state survive recovery. Live dimensions guard shrink-before-paint; a painted resize notice keeps input blocked until a usable replacement paint has flushed. This opt-in protection requires terminal flushing; standalone callers without `minimumSize` retain their existing small-screen behavior.
|
|
56
|
+
|
|
57
|
+
Focus and cell-size responses still reach their protocol owners. Releases, pointer packets and bracketed paste cannot activate hidden controls or invoke `handleBlockedInput`. InteractiveMode uses that callback only for configured interrupt/empty-draft exit and safe settings cancellation; interrupt cancels active agent, bash, compaction and branch-summary operations rather than invoking hidden component handlers. A whitespace-only draft is not empty. The cutoff is an emergency bound, not a guarantee that every custom control fits above it.
|
|
58
|
+
|
|
59
|
+
### Standalone legacy and committed modes
|
|
60
|
+
|
|
61
|
+
Unconfigured TUI callers retain legacy rendering. `setLiveRegionStart(component)` instead selects an append-only committed prefix and a tail-windowed live canvas, with `commit()` scheduling promotion and `commitNow({ requireFlush: true })` requiring successful flush settlement. Detaching that direct child returns to legacy rendering. These public modes remain separate from InteractiveMode's viewport; their native-history behavior has not been redefined. `rebuild()` deliberately repaints their retained history and can bottom-anchor it.
|
|
62
|
+
|
|
9
63
|
## Component Interface
|
|
10
64
|
|
|
11
65
|
All components implement:
|
|
@@ -13,6 +67,7 @@ All components implement:
|
|
|
13
67
|
```typescript
|
|
14
68
|
interface Component {
|
|
15
69
|
render(width: number): string[];
|
|
70
|
+
setViewportHeight?(height: number | undefined): void;
|
|
16
71
|
handleInput?(data: string): void;
|
|
17
72
|
wantsKeyRelease?: boolean;
|
|
18
73
|
invalidate(): void;
|
|
@@ -22,7 +77,8 @@ interface Component {
|
|
|
22
77
|
| Method | Description |
|
|
23
78
|
|--------|-------------|
|
|
24
79
|
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
|
25
|
-
| `
|
|
80
|
+
| `setViewportHeight?(height)` | Optional image-sizing bound. Forward through custom image wrappers; never truncate ordinary logical text. |
|
|
81
|
+
| `handleInput?(data)` | Receive keyboard input when component has focus. Use key parsers, not raw-byte equality. |
|
|
26
82
|
| `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
|
|
27
83
|
| `invalidate()` | Clear cached render state. Called on theme changes. |
|
|
28
84
|
|
|
@@ -90,22 +146,53 @@ Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) w
|
|
|
90
146
|
|
|
91
147
|
```typescript
|
|
92
148
|
pi.on("session_start", async (_event, ctx) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
await ctx.ui.custom<void>((_tui, theme, keybindings, done) => ({
|
|
150
|
+
render: (width) => [truncateToWidth(theme.fg("accent", "Enter to close"), width)],
|
|
151
|
+
handleInput(data) {
|
|
152
|
+
if (keybindings.matches(data, "tui.select.confirm")) done();
|
|
153
|
+
},
|
|
154
|
+
invalidate() {},
|
|
155
|
+
}));
|
|
96
156
|
});
|
|
97
157
|
```
|
|
98
158
|
|
|
99
|
-
**In custom tools** via `
|
|
159
|
+
**In custom tools** via `ctx.ui.custom()`:
|
|
100
160
|
|
|
101
161
|
```typescript
|
|
102
162
|
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
|
103
|
-
const
|
|
163
|
+
const result = await ctx.ui.custom((_tui, _theme, _keybindings, done) => myComponent(done));
|
|
104
164
|
// ...
|
|
105
|
-
handle.close();
|
|
106
165
|
}
|
|
107
166
|
```
|
|
108
167
|
|
|
168
|
+
### Allocated custom input height
|
|
169
|
+
|
|
170
|
+
Ordinary editor-slot `ctx.ui.custom(factory, { onAvailableHeight })` can explicitly opt into the renderer's input-row allocation. The synchronous callback receives `undefined` on mounting/release and the numeric allocation immediately before rendering; it can run more than once in a frame. Store the value for rendering without requesting another render or changing focus. Numeric allocation is not proof of docking, visibility or flushing. Retained undocked layouts can still allocate rows; committed mode does not supply a numeric budget. Below-minimum geometry suppresses rendering rather than sending a temporary budget; recovery allocates before the next render.
|
|
171
|
+
|
|
172
|
+
The callback is invocation-bound and is rejected with `overlay: true` or `toolAttachedContext` before the factory runs. Components without this explicit option are unchanged, including components with a `setMaxHeight` method. This is separate from editor text-height preferences and image-only `setViewportHeight`. A selector may use the budget for a selection-following row window, including partial neighboring choices, without changing its item-based navigation or acceptance semantics.
|
|
173
|
+
|
|
174
|
+
### Tool-attached retained context
|
|
175
|
+
|
|
176
|
+
A pending sequential tool can pair compact live controls with immutable long-form context retained at that tool's transcript position:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const result = await ctx.ui.custom(
|
|
180
|
+
(_tui, _theme, _keybindings, done) => new ApprovalSelector(done),
|
|
181
|
+
{
|
|
182
|
+
toolAttachedContext: {
|
|
183
|
+
toolCallId,
|
|
184
|
+
render: (_tui, theme) => new Markdown(completePayload, 0, 0, getMarkdownTheme()),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The context is constructed after the controls factory resolves and installed completely at the named pending tool row. In both renderer modes, the editor is defocused and the required initial flush must settle before controls receive focus. Missing/non-leading/settled rows, context replaced before that flush settles, and missing or failed flushing reject the request through attachment cleanup. The context is visual-only, not persisted or sent to the model. Use this only from the sequential tool identified by `toolCallId`.
|
|
191
|
+
|
|
192
|
+
In **retained mode**, context taller than the screen remains browseable through the application scrollbar without simultaneous visibility or native-scrollback emission. Controls are revealed before focusing and require current-paint flush evidence, complete context/control rendering and whole-control visibility. Clipped context, including offscreen rows, or controls too tall to fit fail closed. Browsing away consumes the first activation to reveal and flush controls; only a subsequent activation can authorize. Input rechecks attachment currentness and presentation guards.
|
|
193
|
+
|
|
194
|
+
In **committed compatibility mode**, the complete context is emitted into native history by `commitNow({ requireFlush: true })` before controls receive focus. This is an initial committed-context flush contract, not retained visibility protection: the mutable canvas is tail-windowed, with no application scrollbar, retained completeness/whole-control-visibility checks, or reveal-before-subsequent-activation guard.
|
|
195
|
+
|
|
109
196
|
## Overlays
|
|
110
197
|
|
|
111
198
|
Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
|
|
@@ -367,22 +454,20 @@ pi.registerCommand("pick", {
|
|
|
367
454
|
description: "Pick an item",
|
|
368
455
|
handler: async (args, ctx) => {
|
|
369
456
|
const items = ["Option A", "Option B", "Option C"];
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
handle.close();
|
|
382
|
-
resolve();
|
|
457
|
+
const result = await ctx.ui.custom<string | undefined>((tui, _theme, _kb, done) => {
|
|
458
|
+
const selector = new MySelector(items);
|
|
459
|
+
selector.onSelect = (item) => done(item);
|
|
460
|
+
selector.onCancel = () => done(undefined);
|
|
461
|
+
return {
|
|
462
|
+
render: (width) => selector.render(width),
|
|
463
|
+
invalidate: () => selector.invalidate(),
|
|
464
|
+
handleInput(data) {
|
|
465
|
+
selector.handleInput(data);
|
|
466
|
+
tui.requestRender();
|
|
467
|
+
},
|
|
383
468
|
};
|
|
384
|
-
handle = ctx.ui.custom(selector);
|
|
385
469
|
});
|
|
470
|
+
if (result !== undefined) ctx.ui.notify(`Selected: ${result}`, "info");
|
|
386
471
|
}
|
|
387
472
|
});
|
|
388
473
|
```
|
|
@@ -710,6 +795,8 @@ pi.registerCommand("settings", {
|
|
|
710
795
|
container.addChild(settingsList);
|
|
711
796
|
|
|
712
797
|
return {
|
|
798
|
+
get focused() { return settingsList.focused; },
|
|
799
|
+
set focused(value) { settingsList.focused = value; },
|
|
713
800
|
render: (w) => container.render(w),
|
|
714
801
|
invalidate: () => container.invalidate(),
|
|
715
802
|
handleInput: (data) => settingsList.handleInput?.(data),
|
|
@@ -719,6 +806,8 @@ pi.registerCommand("settings", {
|
|
|
719
806
|
});
|
|
720
807
|
```
|
|
721
808
|
|
|
809
|
+
With `enableSearch`, typing fuzzy-filters labels, Backspace edits the query, and Up/Down plus Enter/Space navigate and activate matches. `SettingsList` propagates focus to its search input and nested focusable submenus for IME cursor placement; a wrapper returned from `ctx.ui.custom()` must forward `focused` as shown above.
|
|
810
|
+
|
|
722
811
|
**Examples:** [tools.ts](../examples/extensions/tools.ts)
|
|
723
812
|
|
|
724
813
|
### Pattern 4: Persistent Status Indicator
|
|
@@ -823,7 +912,7 @@ Replace the main input editor with a custom implementation. Useful for modal edi
|
|
|
823
912
|
|
|
824
913
|
```typescript
|
|
825
914
|
import { CustomEditor, type ExtensionAPI } from "@leanandmean/coding-agent";
|
|
826
|
-
import { matchesKey, truncateToWidth } from "@leanandmean/tui";
|
|
915
|
+
import { decodeKittyPrintable, matchesKey, truncateToWidth } from "@leanandmean/tui";
|
|
827
916
|
|
|
828
917
|
type Mode = "normal" | "insert";
|
|
829
918
|
|
|
@@ -848,8 +937,14 @@ class VimEditor extends CustomEditor {
|
|
|
848
937
|
return;
|
|
849
938
|
}
|
|
850
939
|
|
|
940
|
+
if (matchesKey(data, "enter")) {
|
|
941
|
+
super.handleInput(data);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
851
945
|
// Normal mode: vim-style navigation
|
|
852
|
-
|
|
946
|
+
const key = decodeKittyPrintable(data) ?? data;
|
|
947
|
+
switch (key) {
|
|
853
948
|
case "i": this.mode = "insert"; return;
|
|
854
949
|
case "h": super.handleInput("\x1b[D"); return; // Left
|
|
855
950
|
case "j": super.handleInput("\x1b[B"); return; // Down
|
|
@@ -857,14 +952,14 @@ class VimEditor extends CustomEditor {
|
|
|
857
952
|
case "l": super.handleInput("\x1b[C"); return; // Right
|
|
858
953
|
}
|
|
859
954
|
// Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
|
|
860
|
-
if (
|
|
955
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) return;
|
|
861
956
|
super.handleInput(data);
|
|
862
957
|
}
|
|
863
958
|
|
|
864
959
|
render(width: number): string[] {
|
|
865
960
|
const lines = super.render(width);
|
|
866
961
|
// Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
|
|
867
|
-
if (lines.length
|
|
962
|
+
if (lines.length >= 3) {
|
|
868
963
|
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
869
964
|
const lastLine = lines[lines.length - 1]!;
|
|
870
965
|
// Pass "" as ellipsis to avoid adding "..." when truncating
|
package/docs/usage.md
CHANGED
|
@@ -11,7 +11,9 @@ The interface has four main areas:
|
|
|
11
11
|
- **Startup header** - shortcuts, loaded context files, prompt templates, skills, and extensions
|
|
12
12
|
- **Messages** - user messages, assistant responses, tool calls, tool results, notifications, errors, and extension UI
|
|
13
13
|
- **Editor** - where you type; border color indicates the current thinking level
|
|
14
|
-
- **Footer** - working directory, session name, token/cache usage, cost, context usage, and
|
|
14
|
+
- **Footer** - working directory, session name, token/cache usage, cost, context usage, current model, and output speed when valid data exists
|
|
15
|
+
|
|
16
|
+
The footer shows the current model's recent median output-rate estimate beside a nonzero session output total when valid history exists. The `/model` and `/scoped-models` selectors annotate every model row that has valid history. Completed and historical estimates use normalized final output usage over the interval from the first to last generated output; they exclude time to first token and trailing completion latency.
|
|
15
17
|
|
|
16
18
|
The editor can be replaced temporarily by built-in UI such as `/settings` or by custom extension UI.
|
|
17
19
|
|
|
@@ -92,13 +94,13 @@ See [Sessions](sessions.md) and [Compaction](compaction.md) for details.
|
|
|
92
94
|
|
|
93
95
|
## Context Files
|
|
94
96
|
|
|
95
|
-
Scramjet loads `
|
|
97
|
+
Scramjet loads `CLAUDE.md` and `AGENTS.md` at startup from:
|
|
96
98
|
|
|
97
|
-
- `~/.scramjet/agent
|
|
99
|
+
- `~/.scramjet/agent/` for global instructions
|
|
98
100
|
- parent directories, walking up from the current working directory
|
|
99
101
|
- the current directory
|
|
100
102
|
|
|
101
|
-
Use context files for project conventions, commands, safety rules, and preferences. Disable loading with `--no-context-files` or `-nc`.
|
|
103
|
+
Files load broadest-to-most-specific, with `CLAUDE.md` before `AGENTS.md` in each directory. When both names in one directory decode to exactly equal text, only `CLAUDE.md` is loaded; equality suppression never crosses directory boundaries. Use context files for project conventions, commands, safety rules, and preferences. Disable loading with `--no-context-files` or `-nc`.
|
|
102
104
|
|
|
103
105
|
### System Prompt Files
|
|
104
106
|
|
|
@@ -49,11 +49,10 @@ function formatCwd(cwd: string): string {
|
|
|
49
49
|
|
|
50
50
|
function formatContext(ctx: ExtensionContext): string {
|
|
51
51
|
const usage = ctx.getContextUsage();
|
|
52
|
-
|
|
53
|
-
if (!contextWindow || !usage || usage.percent === null) {
|
|
52
|
+
if (!usage || usage.percent === null) {
|
|
54
53
|
return "ctx ?";
|
|
55
54
|
}
|
|
56
|
-
return `ctx ${Math.round(usage.percent)}%/${(contextWindow / 1000).toFixed(0)}k`;
|
|
55
|
+
return `ctx ${Math.round(usage.percent)}%/${(usage.contextWindow / 1000).toFixed(0)}k`;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
function formatThinking(level: string): string {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Component } from "@leanandmean/tui";
|
|
9
|
-
import { isKeyRelease, type TUI } from "@leanandmean/tui";
|
|
9
|
+
import { isKeyRelease, matchesKey, type TUI } from "@leanandmean/tui";
|
|
10
10
|
import type { DoomEngine } from "./doom-engine.js";
|
|
11
11
|
import { DoomKeys, mapKeyToDoom } from "./doom-keys.js";
|
|
12
12
|
|
|
@@ -81,8 +81,8 @@ export class DoomOverlayComponent implements Component {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
handleInput(data: string): void {
|
|
84
|
-
//
|
|
85
|
-
if (!isKeyRelease(data) && (data
|
|
84
|
+
// SCRAMJET-DIVERGENCE: viewport terminals explicitly encode printable pause/exit keys.
|
|
85
|
+
if (!isKeyRelease(data) && (matchesKey(data, "q") || matchesKey(data, "shift+q"))) {
|
|
86
86
|
// Send DOOM's pause key before exiting
|
|
87
87
|
this.engine.pushKey(true, DoomKeys.KEY_PAUSE);
|
|
88
88
|
this.engine.pushKey(false, DoomKeys.KEY_PAUSE);
|
|
@@ -26,11 +26,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
26
26
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
27
27
|
|
|
28
28
|
pi.on("session_start", async (_event, ctx) => {
|
|
29
|
-
|
|
29
|
+
// SCRAMJET-DIVERGENCE: builtin themes renamed to pi-dark/pi-light.
|
|
30
|
+
let currentTheme = (await isDarkMode()) ? "pi-dark" : "pi-light";
|
|
30
31
|
ctx.ui.setTheme(currentTheme);
|
|
31
32
|
|
|
32
33
|
intervalId = setInterval(async () => {
|
|
33
|
-
const newTheme = (await isDarkMode()) ? "dark" : "light";
|
|
34
|
+
const newTheme = (await isDarkMode()) ? "pi-dark" : "pi-light";
|
|
34
35
|
if (newTheme !== currentTheme) {
|
|
35
36
|
currentTheme = newTheme;
|
|
36
37
|
ctx.ui.setTheme(currentTheme);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { CustomEditor, type ExtensionAPI } from "@leanandmean/coding-agent";
|
|
13
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@leanandmean/tui";
|
|
13
|
+
import { decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth } from "@leanandmean/tui";
|
|
14
14
|
|
|
15
15
|
// Normal mode key mappings: key -> escape sequence (or null for mode switch)
|
|
16
16
|
const NORMAL_KEYS: Record<string, string | null> = {
|
|
@@ -45,12 +45,17 @@ class ModalEditor extends CustomEditor {
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
if (data
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// SCRAMJET-DIVERGENCE: viewport terminals can encode ordinary printable keys explicitly.
|
|
49
|
+
if (matchesKey(data, "enter")) {
|
|
50
|
+
super.handleInput(data);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const key = decodeKittyPrintable(data) ?? data;
|
|
54
|
+
if (key in NORMAL_KEYS) {
|
|
55
|
+
const seq = NORMAL_KEYS[key];
|
|
56
|
+
if (key === "i") {
|
|
52
57
|
this.mode = "insert";
|
|
53
|
-
} else if (
|
|
58
|
+
} else if (key === "a") {
|
|
54
59
|
this.mode = "insert";
|
|
55
60
|
super.handleInput("\x1b[C"); // move right first
|
|
56
61
|
} else if (seq) {
|
|
@@ -60,13 +65,13 @@ class ModalEditor extends CustomEditor {
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
63
|
-
if (
|
|
68
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) return;
|
|
64
69
|
super.handleInput(data);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
render(width: number): string[] {
|
|
68
73
|
const lines = super.render(width);
|
|
69
|
-
if (lines.length
|
|
74
|
+
if (lines.length < 3) return lines;
|
|
70
75
|
|
|
71
76
|
// Add mode indicator to bottom border
|
|
72
77
|
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@leanandmean/coding-agent";
|
|
23
23
|
import type { Component, OverlayAnchor, OverlayHandle, OverlayOptions, TUI } from "@leanandmean/tui";
|
|
24
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@leanandmean/tui";
|
|
24
|
+
import { decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth } from "@leanandmean/tui";
|
|
25
25
|
import { spawn } from "child_process";
|
|
26
26
|
|
|
27
27
|
// Global handle for toggle demo (in real code, use a more elegant pattern)
|
|
@@ -951,15 +951,21 @@ class PassiveDemoController extends BaseOverlay {
|
|
|
951
951
|
|
|
952
952
|
handleInput(data: string): void {
|
|
953
953
|
this.inputCount++;
|
|
954
|
-
this.lastInputDebug = `len=${data.length} c0=${data.charCodeAt(0)}`;
|
|
955
954
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
956
955
|
this.cleanup();
|
|
957
956
|
this.done();
|
|
958
|
-
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (matchesKey(data, "backspace")) {
|
|
959
960
|
this.typed = this.typed.slice(0, -1);
|
|
960
|
-
|
|
961
|
-
this.typed += data;
|
|
961
|
+
return;
|
|
962
962
|
}
|
|
963
|
+
if (matchesKey(data, "enter")) return;
|
|
964
|
+
|
|
965
|
+
// SCRAMJET-DIVERGENCE: retained terminals encode printable input explicitly.
|
|
966
|
+
const printable = decodeKittyPrintable(data) ?? data;
|
|
967
|
+
this.lastInputDebug = `len=${printable.length} c0=${printable.charCodeAt(0)}`;
|
|
968
|
+
if (printable.length === 1 && printable.charCodeAt(0) >= 32) this.typed += printable;
|
|
963
969
|
}
|
|
964
970
|
|
|
965
971
|
render(width: number): string[] {
|
|
@@ -1303,13 +1309,20 @@ class StreamingInputPanel implements Component {
|
|
|
1303
1309
|
handleInput(data: string): void {
|
|
1304
1310
|
if (matchesKey(data, "tab")) {
|
|
1305
1311
|
this.onTab();
|
|
1306
|
-
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
1307
1315
|
this.onClose();
|
|
1308
|
-
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (matchesKey(data, "backspace")) {
|
|
1309
1319
|
this.typed = this.typed.slice(0, -1);
|
|
1310
|
-
|
|
1311
|
-
this.typed += data;
|
|
1320
|
+
return;
|
|
1312
1321
|
}
|
|
1322
|
+
if (matchesKey(data, "enter")) return;
|
|
1323
|
+
|
|
1324
|
+
const printable = decodeKittyPrintable(data) ?? data;
|
|
1325
|
+
if (printable.length === 1 && printable.charCodeAt(0) >= 32) this.typed += printable;
|
|
1313
1326
|
}
|
|
1314
1327
|
|
|
1315
1328
|
render(width: number): string[] {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@leanandmean/coding-agent";
|
|
12
|
-
import { CURSOR_MARKER, type Focusable, matchesKey, visibleWidth } from "@leanandmean/tui";
|
|
12
|
+
import { CURSOR_MARKER, decodeKittyPrintable, type Focusable, matchesKey, visibleWidth } from "@leanandmean/tui";
|
|
13
13
|
|
|
14
14
|
export default function (pi: ExtensionAPI) {
|
|
15
15
|
pi.registerCommand("overlay-test", {
|
|
@@ -48,6 +48,8 @@ class OverlayTestComponent implements Focusable {
|
|
|
48
48
|
) {}
|
|
49
49
|
|
|
50
50
|
handleInput(data: string): void {
|
|
51
|
+
// SCRAMJET-DIVERGENCE: retained terminals encode printable input explicitly.
|
|
52
|
+
const printable = decodeKittyPrintable(data) ?? data;
|
|
51
53
|
if (matchesKey(data, "escape")) {
|
|
52
54
|
this.done(undefined);
|
|
53
55
|
return;
|
|
@@ -74,8 +76,8 @@ class OverlayTestComponent implements Focusable {
|
|
|
74
76
|
current.cursor = Math.max(0, current.cursor - 1);
|
|
75
77
|
} else if (matchesKey(data, "right")) {
|
|
76
78
|
current.cursor = Math.min(current.text.length, current.cursor + 1);
|
|
77
|
-
} else if (
|
|
78
|
-
current.text = current.text.slice(0, current.cursor) +
|
|
79
|
+
} else if (printable.length === 1 && printable.charCodeAt(0) >= 32) {
|
|
80
|
+
current.text = current.text.slice(0, current.cursor) + printable + current.text.slice(current.cursor);
|
|
79
81
|
current.cursor++;
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@leanandmean/coding-agent";
|
|
6
|
-
import { matchesKey, visibleWidth } from "@leanandmean/tui";
|
|
6
|
+
import { decodeKittyPrintable, matchesKey, visibleWidth } from "@leanandmean/tui";
|
|
7
7
|
|
|
8
8
|
const GAME_WIDTH = 40;
|
|
9
9
|
const GAME_HEIGHT = 15;
|
|
@@ -148,6 +148,8 @@ class SnakeComponent {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
handleInput(data: string): void {
|
|
151
|
+
// SCRAMJET-DIVERGENCE: retained terminals encode printable input explicitly.
|
|
152
|
+
data = decodeKittyPrintable(data) ?? data;
|
|
151
153
|
// If paused (resuming), wait for any key
|
|
152
154
|
if (this.paused) {
|
|
153
155
|
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
@@ -319,10 +319,12 @@ class SpaceInvadersComponent {
|
|
|
319
319
|
|
|
320
320
|
handleInput(data: string): void {
|
|
321
321
|
const released = isKeyRelease(data);
|
|
322
|
+
// SCRAMJET-DIVERGENCE: viewport terminals explicitly encode printable quit keys.
|
|
323
|
+
const quit = matchesKey(data, "q") || matchesKey(data, "shift+q");
|
|
322
324
|
|
|
323
325
|
// Pause handling
|
|
324
326
|
if (this.paused && !released) {
|
|
325
|
-
if (matchesKey(data, Key.escape) ||
|
|
327
|
+
if (matchesKey(data, Key.escape) || quit) {
|
|
326
328
|
this.dispose();
|
|
327
329
|
this.onClose();
|
|
328
330
|
return;
|
|
@@ -341,7 +343,7 @@ class SpaceInvadersComponent {
|
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
// Q to quit without saving
|
|
344
|
-
if (!released &&
|
|
346
|
+
if (!released && quit) {
|
|
345
347
|
this.dispose();
|
|
346
348
|
this.onSave(null);
|
|
347
349
|
this.onClose();
|
|
@@ -363,7 +365,8 @@ class SpaceInvadersComponent {
|
|
|
363
365
|
|
|
364
366
|
// Restart on game over or victory
|
|
365
367
|
if (!released && (this.state.gameOver || this.state.victory)) {
|
|
366
|
-
|
|
368
|
+
// SCRAMJET-DIVERGENCE: restart keys also arrive as explicit Kitty sequences.
|
|
369
|
+
if (matchesKey(data, "r") || matchesKey(data, "shift+r") || matchesKey(data, Key.space)) {
|
|
367
370
|
const highScore = this.state.highScore;
|
|
368
371
|
const nextLevel = this.state.victory ? this.state.level + 1 : 1;
|
|
369
372
|
this.state = createInitialState(highScore, nextLevel);
|