@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.0
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/README.md +171 -124
- package/bin/dsh-tui.js +748 -22
- package/cordis.patch.yml +38 -10
- package/cordis.yml +2 -0
- package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
- package/lib/types/adapter/channel/session-projection.js +2 -0
- package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
- package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
- package/lib/types/adapter/channel/ui-policy.js +5 -1
- package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
- package/lib/types/adapter/kernel/host-facade.js +1 -0
- package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
- package/lib/types/adapter/kernel/replay.js +3 -0
- package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
- package/lib/types/adapter/kernel/runtime.js +2 -0
- package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
- package/lib/types/adapter/kernel/slices/workspace.js +1 -0
- package/lib/types/adapter/ports/channel-ui.d.ts +22 -2
- package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts +66 -0
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
- package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
- package/lib/types/adapter/ports/workspace.d.ts +2 -0
- package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.js +10 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +1 -0
- package/lib/types/components/ContextBarView.d.ts +15 -7
- package/lib/types/components/ContextBarView.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.js +26 -22
- package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
- package/lib/types/components/ImagePreviewOverlay.js +11 -5
- package/lib/types/components/Markdown.d.ts +11 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +20 -2
- package/lib/types/components/MermaidDiagram.d.ts +13 -0
- package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
- package/lib/types/components/MermaidDiagram.js +61 -0
- package/lib/types/components/MessageList.d.ts +26 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +72 -7
- package/lib/types/components/PageMargin.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts +37 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +243 -28
- package/lib/types/components/ScrollbarGutter.d.ts +20 -5
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +88 -10
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +30 -28
- package/lib/types/components/Tooltip.d.ts.map +1 -1
- package/lib/types/components/Tooltip.js +45 -7
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +5 -7
- package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
- package/lib/types/components/messages/useImageInspection.js +19 -9
- package/lib/types/components/promptDraftCache.d.ts +102 -0
- package/lib/types/components/promptDraftCache.d.ts.map +1 -0
- package/lib/types/components/promptDraftCache.js +76 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
- package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
- package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +52 -5
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/bundled-presets.js +80 -0
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
- package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/background-action.js +52 -29
- package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
- package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding.js +78 -18
- package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
- package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
- package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
- package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +29 -19
- package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/reports.js +4 -12
- package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +3 -0
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.js +1 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +2 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts +11 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-resume.js +291 -94
- package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
- package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
- package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
- package/lib/types/dsh-adapter/channel/state.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/state.js +1 -1
- package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/transcript.js +6 -12
- package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +67 -6
- package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/messages.js +15 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts +33 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/settings.js +43 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/shell.js +11 -0
- package/lib/types/dsh-adapter/contract.d.ts +4 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +15 -6
- package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
- package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
- package/lib/types/dsh-adapter/ide-channel.js +556 -0
- package/lib/types/dsh-adapter/index.d.ts +18 -3
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +32 -5
- package/lib/types/dsh-adapter/plugin.d.ts +5 -2
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +173 -42
- package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
- package/lib/types/dsh-adapter/preset-resolution.js +10 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessionTree.js +3 -2
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +6 -5
- package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
- package/lib/types/dsh-adapter/workspaces.js +116 -0
- package/lib/types/effortPrefs.d.ts +15 -3
- package/lib/types/effortPrefs.d.ts.map +1 -1
- package/lib/types/effortPrefs.js +37 -3
- package/lib/types/homePrefs.d.ts +15 -0
- package/lib/types/homePrefs.d.ts.map +1 -0
- package/lib/types/homePrefs.js +53 -0
- package/lib/types/i18n.d.ts +257 -296
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +78 -84
- package/lib/types/ink/colorize.d.ts +10 -0
- package/lib/types/ink/colorize.d.ts.map +1 -1
- package/lib/types/ink/colorize.js +21 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +12 -1
- package/lib/types/ink/dom.d.ts +1 -0
- package/lib/types/ink/dom.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +36 -1
- package/lib/types/ink/output.d.ts +10 -1
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +109 -17
- package/lib/types/ink/render-border.d.ts +6 -1
- package/lib/types/ink/render-border.d.ts.map +1 -1
- package/lib/types/ink/render-border.js +25 -9
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +48 -6
- package/lib/types/ink/renderer.d.ts +6 -0
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +2 -1
- package/lib/types/ink/selection.d.ts +60 -0
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +129 -0
- package/lib/types/ink/sixel-graphics.d.ts +19 -1
- package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
- package/lib/types/ink/sixel-graphics.js +124 -12
- package/lib/types/ink/terminal-image.d.ts +18 -1
- package/lib/types/ink/terminal-image.d.ts.map +1 -1
- package/lib/types/ink/terminal.d.ts +17 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +20 -1
- package/lib/types/screens/Chat.d.ts +17 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +284 -72
- package/lib/types/screens/SessionSupervisor.d.ts +84 -0
- package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/SessionSupervisor.js +350 -0
- package/lib/types/screens/SessionTree.js +2 -2
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +32 -9
- package/lib/types/screens/StatusLine.d.ts +13 -0
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +36 -17
- package/lib/types/screens/StatusMetrics.d.ts +79 -22
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +153 -88
- package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/model.js +69 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
- package/lib/types/sessionMounts.d.ts +272 -0
- package/lib/types/sessionMounts.d.ts.map +1 -0
- package/lib/types/sessionMounts.js +706 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +4 -1
- package/lib/types/sessions/resumeFailure.d.ts +38 -0
- package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
- package/lib/types/sessions/resumeFailure.js +58 -0
- package/lib/types/terminal-utils/mermaid.d.ts +44 -0
- package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
- package/lib/types/terminal-utils/mermaid.js +83 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +175 -19
- package/lib/types/tuiDisplayPrefs.d.ts +7 -9
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +31 -28
- package/lib/types/update.d.ts +61 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +194 -0
- package/lib/types/utils/imageResize.d.ts +90 -0
- package/lib/types/utils/imageResize.d.ts.map +1 -0
- package/lib/types/utils/imageResize.js +272 -0
- package/lib/types/utils/keymap.d.ts +5 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +23 -0
- package/lib/types/workspaces.d.ts +2 -1
- package/lib/types/workspaces.d.ts.map +1 -1
- package/lib/types/workspaces.js +1 -1
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +10 -10
- package/package.json +120 -103
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/agent.cordis.yml +15 -0
- package/lib/types/screens/AgentView.d.ts +0 -23
- package/lib/types/screens/AgentView.d.ts.map +0 -1
- package/lib/types/screens/AgentView.js +0 -546
- package/lib/types/screens/SessionBrowser.d.ts +0 -42
- package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
- package/lib/types/screens/SessionBrowser.js +0 -911
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text, NoSelect } from '../ui.js';
|
|
4
|
+
import { FRAME_INTERVAL_MS } from '../ink/constants.js';
|
|
4
5
|
import { RAIL_MIN_TERMINAL_WIDTH, RAIL_WIDTH } from '../ink/timeline-rail.js';
|
|
5
6
|
/** Thumb glyph across the 2-col gutter: solid, clearly positional —
|
|
6
7
|
* deliberately distinct from the timeline's ━━ active tick. */
|
|
7
8
|
const THUMB = '██';
|
|
8
9
|
/** Rest time before the hover position chip pops (anti-flash sweep gate). */
|
|
9
10
|
const CHIP_DWELL_MS = 250;
|
|
11
|
+
/**
|
|
12
|
+
* Post-landing settle reads (see the subscribe effect): how many deferred
|
|
13
|
+
* re-reads follow a landing before giving up. The re-pinning Ink pass lands
|
|
14
|
+
* a frame or two after the commit, so a single read can still catch the
|
|
15
|
+
* pre-landing geometry — the bound only exists so a frame that never
|
|
16
|
+
* settles cannot chain renders forever.
|
|
17
|
+
*/
|
|
18
|
+
const PIN_SETTLE_CHECKS = 3;
|
|
10
19
|
/**
|
|
11
20
|
* Proportional scrollbar for the fullscreen transcript's gutter — the
|
|
12
21
|
* `scrollbar` option of the `dsh-tui.scrollGutter` setting (the timeline
|
|
@@ -17,19 +26,38 @@ const CHIP_DWELL_MS = 250;
|
|
|
17
26
|
* - clicking the track scrolls the clicked position to the viewport top
|
|
18
27
|
* (classic scrollbar semantics — the thumb centers under the click
|
|
19
28
|
* through the follow-up renders);
|
|
29
|
+
* - dragging the track scrubs the transcript: the drag protocol's
|
|
30
|
+
* per-target localRow maps through the SAME trackScrollTop as a click
|
|
31
|
+
* (absolute mapping — drag to point, no grabbed thumb offset), so a
|
|
32
|
+
* drag is a continuous run of click-to-position jumps. The mapping only
|
|
33
|
+
* reads the render-time maxScroll/trackH and never re-reads scrollTop
|
|
34
|
+
* mid-gesture, so a throttled/clamped scrollTo cannot feed back into the
|
|
35
|
+
* pointer mapping (no jitter). Dragging the thumb therefore lands the
|
|
36
|
+
* pointer's row at the viewport top, exactly like clicking that row;
|
|
37
|
+
* - a landing (drag or the back-to-bottom affordance) breaks and re-pins
|
|
38
|
+
* sticky, which unmounts the chrome that had shrunk the transcript row;
|
|
39
|
+
* the re-pinning Ink pass follows the commit and fires no subscriber
|
|
40
|
+
* notify, so the gutter re-reads the handle once that frame lands (see
|
|
41
|
+
* the subscribe effect). Without it the thumb stays painted from the
|
|
42
|
+
* shrunken viewport and floats a few rows above the bottom until an
|
|
43
|
+
* unrelated render (hover) re-reads the handle;
|
|
20
44
|
* - the gutter is permanent while scrollable (Qwen's rule: an
|
|
21
45
|
* auto-hiding gutter that changes content width rewraps everything);
|
|
22
46
|
* hidden below 60 terminal columns or when the content fits (inline
|
|
23
47
|
* mode keeps the terminal's native scrollback);
|
|
24
48
|
* - NoSelect fences the glyphs out of click-drag text selection; the
|
|
25
|
-
* wheel over the gutter scrolls the transcript.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
49
|
+
* wheel over the gutter scrolls the transcript. Because the track is a
|
|
50
|
+
* drag target, an unmodified left drag scrubs instead of selecting, and
|
|
51
|
+
* multi-clicks on the track no longer feed the select-line chain;
|
|
52
|
+
* Shift/Alt/Ctrl drags never open a drag session and keep the
|
|
53
|
+
* selection path.
|
|
30
54
|
*/
|
|
31
55
|
export function ScrollbarGutter({ handle, terminalWidth, }) {
|
|
32
56
|
const [, setTick] = React.useState(0);
|
|
57
|
+
// Geometry the last render painted with. The Ink render pass runs AFTER
|
|
58
|
+
// React's commit, so the commit that lands a pinned view can paint from
|
|
59
|
+
// geometry that pass is about to change (see the subscribe effect).
|
|
60
|
+
const geomRef = React.useRef('');
|
|
33
61
|
// Hover readout: the row under the pointer brightens the thumb when it is
|
|
34
62
|
// the hovered one, and floats a `62% · 340/540` chip left of the gutter
|
|
35
63
|
// naming the position a click there would jump to. The chip is
|
|
@@ -53,12 +81,57 @@ export function ScrollbarGutter({ handle, terminalWidth, }) {
|
|
|
53
81
|
React.useEffect(() => {
|
|
54
82
|
if (!handle)
|
|
55
83
|
return;
|
|
56
|
-
|
|
84
|
+
const readGeom = () => `${handle.getScrollTop()}:${handle.getScrollHeight()}:${handle.getViewportHeight()}`;
|
|
85
|
+
let timer = null;
|
|
86
|
+
let checksLeft = 0;
|
|
87
|
+
// Landing settle re-read. Reaching the bottom breaks and then re-pins
|
|
88
|
+
// sticky; the commit that consumes the notification paints while the
|
|
89
|
+
// chrome that shrank the transcript row (`PinnedTurnHeader` +
|
|
90
|
+
// "return to bottom" pill, both `!isSticky`) is still mounted, and the
|
|
91
|
+
// NEXT Ink pass then grows the viewport back and re-pins scrollTop to
|
|
92
|
+
// the taller maxScroll — with NO subscriber notify, because sticky
|
|
93
|
+
// never flips again (ScrollBox's subscribe contract). Nothing would
|
|
94
|
+
// re-read the handle, so the thumb stays painted from the shrunken
|
|
95
|
+
// viewport and floats above the bottom until an unrelated render
|
|
96
|
+
// (hover) rescues it. Re-read once that pass has landed and re-render
|
|
97
|
+
// if the geometry moved; only pinned notifications arm this (a landing
|
|
98
|
+
// is the one shape that can be re-pinned after the commit), so the
|
|
99
|
+
// cost is a couple of timers per landing, never per scroll frame.
|
|
100
|
+
const settle = () => {
|
|
101
|
+
timer = null;
|
|
102
|
+
if (readGeom() !== geomRef.current)
|
|
103
|
+
setTick(t => t + 1);
|
|
104
|
+
if (--checksLeft > 0)
|
|
105
|
+
timer = setTimeout(settle, FRAME_INTERVAL_MS);
|
|
106
|
+
};
|
|
107
|
+
const unsubscribe = handle.subscribe(() => {
|
|
108
|
+
setTick(t => t + 1);
|
|
109
|
+
if (!handle.isSticky())
|
|
110
|
+
return;
|
|
111
|
+
checksLeft = PIN_SETTLE_CHECKS;
|
|
112
|
+
if (timer === null)
|
|
113
|
+
timer = setTimeout(settle, FRAME_INTERVAL_MS);
|
|
114
|
+
});
|
|
115
|
+
return () => {
|
|
116
|
+
unsubscribe();
|
|
117
|
+
if (timer !== null)
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
};
|
|
57
120
|
}, [handle]);
|
|
121
|
+
// Geometry the last COMMITTED render painted with: captured during render,
|
|
122
|
+
// but the ref write happens in the layout effect below — this root is a
|
|
123
|
+
// ConcurrentRoot, so a render pass can be discarded, and a render-phase
|
|
124
|
+
// write would leak a frame that never painted into settle()'s diff.
|
|
125
|
+
const viewport = handle === null ? 0 : handle.getViewportHeight();
|
|
126
|
+
const content = handle === null ? 0 : handle.getScrollHeight();
|
|
127
|
+
const scrollTop = handle === null ? 0 : handle.getScrollTop();
|
|
128
|
+
const geom = handle === null ? null : `${scrollTop}:${content}:${viewport}`;
|
|
129
|
+
React.useLayoutEffect(() => {
|
|
130
|
+
if (geom !== null)
|
|
131
|
+
geomRef.current = geom;
|
|
132
|
+
}, [geom]);
|
|
58
133
|
if (!handle)
|
|
59
134
|
return null;
|
|
60
|
-
const viewport = handle.getViewportHeight();
|
|
61
|
-
const content = handle.getScrollHeight();
|
|
62
135
|
const maxScroll = Math.max(0, content - viewport);
|
|
63
136
|
if (viewport < 2 || content <= viewport || terminalWidth < RAIL_MIN_TERMINAL_WIDTH)
|
|
64
137
|
return null;
|
|
@@ -67,10 +140,12 @@ export function ScrollbarGutter({ handle, terminalWidth, }) {
|
|
|
67
140
|
// top follows scrollTop over the scroll range.
|
|
68
141
|
const thumbH = Math.max(2, Math.round((viewport * viewport) / content));
|
|
69
142
|
const trackH = Math.max(1, viewport - thumbH);
|
|
70
|
-
const thumbTop = Math.round((
|
|
143
|
+
const thumbTop = Math.round((scrollTop / Math.max(1, maxScroll)) * trackH);
|
|
71
144
|
const thumbBottom = Math.min(viewport, thumbTop + thumbH);
|
|
72
145
|
// Clicking the track maps the clicked row's position on the track back
|
|
73
146
|
// to a scrollTop and scrolls that content position to the viewport top.
|
|
147
|
+
// Dragging reuses the same mapping per motion through the drag protocol's
|
|
148
|
+
// target-relative localRow (the outer track is the single drag target).
|
|
74
149
|
const trackScrollTop = (y) => {
|
|
75
150
|
if (y <= 0)
|
|
76
151
|
return 0;
|
|
@@ -78,6 +153,9 @@ export function ScrollbarGutter({ handle, terminalWidth, }) {
|
|
|
78
153
|
return maxScroll;
|
|
79
154
|
return Math.round((y / trackH) * maxScroll);
|
|
80
155
|
};
|
|
156
|
+
const applyDragRow = (event) => {
|
|
157
|
+
handle.scrollTo(trackScrollTop(event.localRow));
|
|
158
|
+
};
|
|
81
159
|
const rows = [];
|
|
82
160
|
for (let y = 0; y < viewport; y++) {
|
|
83
161
|
const inThumb = y >= thumbTop && y < thumbBottom;
|
|
@@ -115,5 +193,5 @@ export function ScrollbarGutter({ handle, terminalWidth, }) {
|
|
|
115
193
|
return (_jsx(NoSelect, { children: _jsxs("ink-box", { onWheel: e => {
|
|
116
194
|
if (e.deltaY !== 0)
|
|
117
195
|
handle.scrollBy(e.deltaY);
|
|
118
|
-
}, style: { flexDirection: 'column', flexShrink: 0, width: RAIL_WIDTH }, children: [rows, hoverChip] }) }));
|
|
196
|
+
}, onDragStart: applyDragRow, onDragMove: applyDragRow, onDragEnd: applyDragRow, style: { flexDirection: 'column', flexShrink: 0, width: RAIL_WIDTH }, children: [rows, hoverChip] }) }));
|
|
119
197
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamingMarkdown.d.ts","sourceRoot":"","sources":["../../../src/components/StreamingMarkdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"StreamingMarkdown.d.ts","sourceRoot":"","sources":["../../../src/components/StreamingMarkdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAuMzB,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,CAyIlB"}
|
|
@@ -4,7 +4,7 @@ import { marked } from 'marked';
|
|
|
4
4
|
import Box from '../ink/components/Box.js';
|
|
5
5
|
import { formatToken, stripPromptXMLTags } from '../terminal-utils/markdown.js';
|
|
6
6
|
import { t } from '../i18n.js';
|
|
7
|
-
import { Markdown } from './Markdown.js';
|
|
7
|
+
import { isStandaloneToken, Markdown } from './Markdown.js';
|
|
8
8
|
/**
|
|
9
9
|
* Renders streaming markdown in sealed groups of completed top-level blocks.
|
|
10
10
|
* Only the unsealed group and growing final block change as text arrives.
|
|
@@ -50,7 +50,7 @@ function clipSuffixTail(suffix, cut) {
|
|
|
50
50
|
const UNSAFE_BOUNDARY = {
|
|
51
51
|
safe: false,
|
|
52
52
|
gap: 0,
|
|
53
|
-
|
|
53
|
+
endsWithNode: false,
|
|
54
54
|
trailingEmptyTextNode: false,
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
@@ -70,8 +70,8 @@ function blankTokenNewlines(type) {
|
|
|
70
70
|
* Analyze the candidate stable tokens using the same formatter as Markdown.
|
|
71
71
|
* Both split halves trim their outer whitespace, so the trailing newline count
|
|
72
72
|
* determines the Yoga gap: one newline merely starts the next row; every
|
|
73
|
-
* additional newline is one genuinely blank row.
|
|
74
|
-
* nodes and therefore keep Markdown's fixed one-row node gap.
|
|
73
|
+
* additional newline is one genuinely blank row. Standalone nodes are
|
|
74
|
+
* separate layout nodes and therefore keep Markdown's fixed one-row node gap.
|
|
75
75
|
*
|
|
76
76
|
* Only the LAST token that can produce visible text decides the outcome, so
|
|
77
77
|
* the region is walked backwards and formatted one token at a time instead of
|
|
@@ -79,14 +79,14 @@ function blankTokenNewlines(type) {
|
|
|
79
79
|
* boundary. `formatToken` runs once for the common case.
|
|
80
80
|
*/
|
|
81
81
|
function analyzeStableBoundary(tokens, suffixIndex) {
|
|
82
|
-
// A
|
|
83
|
-
// one matters; anything before it is already folded into the
|
|
82
|
+
// A standalone node resets the accumulated text, so only the segment after
|
|
83
|
+
// the last one matters; anything before it is already folded into the node branch.
|
|
84
84
|
let segmentStart = 0;
|
|
85
|
-
let
|
|
85
|
+
let hasNode = false;
|
|
86
86
|
for (let i = suffixIndex - 1; i >= 0; i--) {
|
|
87
|
-
if (tokens[i]
|
|
87
|
+
if (isStandaloneToken(tokens[i])) {
|
|
88
88
|
segmentStart = i + 1;
|
|
89
|
-
|
|
89
|
+
hasNode = true;
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -116,17 +116,17 @@ function analyzeStableBoundary(tokens, suffixIndex) {
|
|
|
116
116
|
return {
|
|
117
117
|
safe: true,
|
|
118
118
|
gap: Math.max(0, trailingNewlines - 1),
|
|
119
|
-
|
|
119
|
+
endsWithNode: false,
|
|
120
120
|
trailingEmptyTextNode: false,
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
-
if (
|
|
123
|
+
if (hasNode) {
|
|
124
124
|
return {
|
|
125
125
|
safe: true,
|
|
126
126
|
gap: 1,
|
|
127
|
-
|
|
128
|
-
// Whitespace after the
|
|
129
|
-
// between two
|
|
127
|
+
endsWithNode: true,
|
|
128
|
+
// Whitespace after the node still occupies a zero-height text node
|
|
129
|
+
// between two standalone nodes.
|
|
130
130
|
trailingEmptyTextNode: blankNewlines > 0,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
@@ -141,14 +141,16 @@ function analyzeStableBoundary(tokens, suffixIndex) {
|
|
|
141
141
|
function analyzeSuffixStart(tokens, startIndex) {
|
|
142
142
|
let leadingNewlines = 0;
|
|
143
143
|
for (let i = startIndex; i < tokens.length; i++) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
if (
|
|
144
|
+
const token = tokens[i];
|
|
145
|
+
if (token === undefined)
|
|
146
|
+
break;
|
|
147
|
+
if (isStandaloneToken(token))
|
|
148
|
+
return { kind: 'node', leadingNewlines };
|
|
149
|
+
if (token.type === 'space' || token.type === 'br') {
|
|
148
150
|
leadingNewlines += 1;
|
|
149
151
|
continue;
|
|
150
152
|
}
|
|
151
|
-
if (type === 'def' || type === 'del' || type === 'html')
|
|
153
|
+
if (token.type === 'def' || token.type === 'del' || token.type === 'html')
|
|
152
154
|
continue;
|
|
153
155
|
return { kind: 'text', leadingNewlines };
|
|
154
156
|
}
|
|
@@ -157,10 +159,10 @@ function analyzeSuffixStart(tokens, startIndex) {
|
|
|
157
159
|
function gapBetween(boundary, start) {
|
|
158
160
|
if (start.kind === undefined)
|
|
159
161
|
return 0;
|
|
160
|
-
if (boundary.
|
|
161
|
-
return start.kind === '
|
|
162
|
+
if (boundary.endsWithNode) {
|
|
163
|
+
return start.kind === 'node' && (boundary.trailingEmptyTextNode || start.leadingNewlines > 0) ? 2 : 1;
|
|
162
164
|
}
|
|
163
|
-
return start.kind === '
|
|
165
|
+
return start.kind === 'node' ? 1 : boundary.gap + start.leadingNewlines;
|
|
164
166
|
}
|
|
165
167
|
export function StreamingMarkdown({ children, dimColor = false, }) {
|
|
166
168
|
// The prefix tracks source offsets; its rendered blocks are sealed once
|
|
@@ -173,7 +175,7 @@ export function StreamingMarkdown({ children, dimColor = false, }) {
|
|
|
173
175
|
const cutRef = React.useRef(0);
|
|
174
176
|
const boundaryGapRef = React.useRef(0);
|
|
175
177
|
const prefixVisibleRef = React.useRef(false);
|
|
176
|
-
const
|
|
178
|
+
const prefixEndsWithNodeRef = React.useRef(false);
|
|
177
179
|
const prefixTrailingEmptyTextRef = React.useRef(false);
|
|
178
180
|
const stripped = stripPromptXMLTags(children);
|
|
179
181
|
// Reset if text was replaced (defensive; normally unmount handles this)
|
|
@@ -182,7 +184,7 @@ export function StreamingMarkdown({ children, dimColor = false, }) {
|
|
|
182
184
|
cutRef.current = 0;
|
|
183
185
|
boundaryGapRef.current = 0;
|
|
184
186
|
prefixVisibleRef.current = false;
|
|
185
|
-
|
|
187
|
+
prefixEndsWithNodeRef.current = false;
|
|
186
188
|
prefixTrailingEmptyTextRef.current = false;
|
|
187
189
|
blocksRef.current = {
|
|
188
190
|
blocks: [], end: 0, tokens: [], tail: '', tailGap: 0,
|
|
@@ -241,7 +243,7 @@ export function StreamingMarkdown({ children, dimColor = false, }) {
|
|
|
241
243
|
prefixRef.current = stripped.substring(0, boundary + advance);
|
|
242
244
|
boundaryGapRef.current = stableBoundary.gap;
|
|
243
245
|
prefixVisibleRef.current = true;
|
|
244
|
-
|
|
246
|
+
prefixEndsWithNodeRef.current = stableBoundary.endsWithNode;
|
|
245
247
|
prefixTrailingEmptyTextRef.current = stableBoundary.trailingEmptyTextNode;
|
|
246
248
|
suffixTokenIndex = lastContentIdx;
|
|
247
249
|
}
|
|
@@ -257,12 +259,12 @@ export function StreamingMarkdown({ children, dimColor = false, }) {
|
|
|
257
259
|
? { kind: 'text', leadingNewlines: 0 }
|
|
258
260
|
: analyzeSuffixStart(tokens, suffixTokenIndex);
|
|
259
261
|
const boundaryGap = prefixVisibleRef.current && suffixStart.kind !== undefined
|
|
260
|
-
?
|
|
261
|
-
? suffixStart.kind === '
|
|
262
|
+
? prefixEndsWithNodeRef.current
|
|
263
|
+
? suffixStart.kind === 'node' &&
|
|
262
264
|
(prefixTrailingEmptyTextRef.current || suffixStart.leadingNewlines > 0)
|
|
263
265
|
? 2
|
|
264
266
|
: 1
|
|
265
|
-
: suffixStart.kind === '
|
|
267
|
+
: suffixStart.kind === 'node'
|
|
266
268
|
? 1
|
|
267
269
|
: boundaryGapRef.current + suffixStart.leadingNewlines
|
|
268
270
|
: 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.d.ts","sourceRoot":"","sources":["../../../src/components/Tooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Tooltip.d.ts","sourceRoot":"","sources":["../../../src/components/Tooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AA0BlE,yEAAyE;AACzE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,CAAC,KAAK,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAAA;AAED;;;;GAIG;AACH,KAAK,YAAY,GAAG;IAAE,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAA;CAAE,CAAA;AAEnE,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAYD,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAKjE;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,YAAY,GAAG,IAAI,CAExD;AAcD,mEAAmE;AACnE,wBAAgB,YAAY,IAAI,IAAI,CAMnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAuC5F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,GAAG,KAAK,CAAC,SAAS,CAGlB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,eAAe,EACf,qBAAqB,GACtB,GAAE;IACD,8EAA8E;IAC9E,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+EAA+E;IAC/E,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CACxD,GAAG,KAAK,CAAC,SAAS,CA8GvB"}
|
|
@@ -3,6 +3,8 @@ import React from 'react';
|
|
|
3
3
|
import stripAnsi from 'strip-ansi';
|
|
4
4
|
import { Box, Text, useTerminalSize } from '../ui.js';
|
|
5
5
|
import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js';
|
|
6
|
+
import { useSelection } from '../ink/hooks/use-selection.js';
|
|
7
|
+
import { hasSelection } from '../ink/selection.js';
|
|
6
8
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
7
9
|
import { getGraphemeSegmenter } from '../utils/intl.js';
|
|
8
10
|
import { PageInsetContext } from './PageMargin.js';
|
|
@@ -17,6 +19,11 @@ import { PageInsetContext } from './PageMargin.js';
|
|
|
17
19
|
* content already visible next to the pointer is noise, not detail (the
|
|
18
20
|
* tool-call header gates on folded/clipped content for this reason).
|
|
19
21
|
*
|
|
22
|
+
* Tooltips and text selection are mutually exclusive: a drag-copy reads the
|
|
23
|
+
* painted screen, and the card REPLACES the cells it covers, so
|
|
24
|
+
* {@link TooltipLayer} hides all tooltips while a selection drag or an
|
|
25
|
+
* active selection exists (see its subscription on selection mutations).
|
|
26
|
+
*
|
|
20
27
|
* Hover events only exist in fullscreen (alternate screen + mouse
|
|
21
28
|
* tracking); inline mode never fires the handlers, so the store can only
|
|
22
29
|
* be written from a live fullscreen tree.
|
|
@@ -135,6 +142,29 @@ export function TooltipLayer({ invalidationKey, subscribeInvalidation, } = {}) {
|
|
|
135
142
|
const { columns, rows } = useTerminalSize();
|
|
136
143
|
const terminalFocused = useTerminalFocus();
|
|
137
144
|
const tooltip = React.useSyncExternalStore(subscribeTooltip, getTooltipSnapshot);
|
|
145
|
+
// A text selection and a floating tooltip are mutually exclusive: the copy
|
|
146
|
+
// reads the PAINTED screen, and the tooltip card REPLACES the cells of the
|
|
147
|
+
// text it covers — dragging across a shown tooltip (or letting one pop
|
|
148
|
+
// mid-drag via its dwell timer) puts the tooltip fragment in the clipboard
|
|
149
|
+
// instead of the message text. Every selection mutation (drag start, each
|
|
150
|
+
// motion event, release, clear) funnels through this notification, so the
|
|
151
|
+
// layer goes dark the instant a drag begins and stays dark until the
|
|
152
|
+
// selection settles. The render guard below is belt-and-suspenders: a
|
|
153
|
+
// dwell that fires between two motion events must still never paint.
|
|
154
|
+
const selection = useSelection();
|
|
155
|
+
const selectionBusy = React.useSyncExternalStore(selection.subscribe, () => {
|
|
156
|
+
const state = selection.getState();
|
|
157
|
+
return state !== null && (state.isDragging || hasSelection(state));
|
|
158
|
+
}, () => false);
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
return selection.subscribe(() => {
|
|
161
|
+
// Every selection mutation (drag start, motion, release, clear) resets
|
|
162
|
+
// the tooltip system wholesale: while busy the card must never paint,
|
|
163
|
+
// and the settle must also invalidate dwell timers armed while dark,
|
|
164
|
+
// so a finished copy is not immediately followed by a surprise card.
|
|
165
|
+
clearTooltip();
|
|
166
|
+
});
|
|
167
|
+
}, [selection]);
|
|
138
168
|
// Remounts and modal/screen transitions invalidate shown AND pending tips.
|
|
139
169
|
React.useEffect(() => {
|
|
140
170
|
clearTooltip();
|
|
@@ -157,6 +187,9 @@ export function TooltipLayer({ invalidationKey, subscribeInvalidation, } = {}) {
|
|
|
157
187
|
}, [columns, rows]);
|
|
158
188
|
if (tooltip === null)
|
|
159
189
|
return null;
|
|
190
|
+
// Never paint over an active drag/selection (see the subscription above).
|
|
191
|
+
if (selectionBusy)
|
|
192
|
+
return null;
|
|
160
193
|
// A bordered card needs at least one content cell plus two border cells.
|
|
161
194
|
// Below that, hiding is safer than creating geometry wider/taller than the
|
|
162
195
|
// terminal (Yoga would clip unpredictably on 1–2 column/row resize states).
|
|
@@ -176,14 +209,19 @@ export function TooltipLayer({ invalidationKey, subscribeInvalidation, } = {}) {
|
|
|
176
209
|
const height = lines.length + 2;
|
|
177
210
|
// Prefer resting on the row ABOVE the anchor; when there is no room
|
|
178
211
|
// (anchor at the top of the screen), drop below it, clamped on-screen.
|
|
179
|
-
// The anchor
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
212
|
+
// The anchor is SCREEN geometry (pointer events) while the absolute box is
|
|
213
|
+
// placed relative to the inset content-area origin, and useTerminalSize()
|
|
214
|
+
// already reports the content size: convert the anchor into content
|
|
215
|
+
// coordinates first, then clamp inside the content area. Adding the inset
|
|
216
|
+
// (the earlier bug) moved the whole card by the page margin — with the
|
|
217
|
+
// default margin one row low and two columns right, so its bottom border
|
|
218
|
+
// landed ON the hovered row and that row's glyphs showed beside the card.
|
|
183
219
|
const inset = React.useContext(PageInsetContext);
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
220
|
+
const anchorRow = tooltip.anchorRow - inset.y;
|
|
221
|
+
const anchorCol = tooltip.anchorCol - inset.x;
|
|
222
|
+
const topAbove = anchorRow - height;
|
|
223
|
+
const top = Math.max(0, Math.min(topAbove >= 0 ? topAbove : anchorRow + 1, Math.max(0, rows - height)));
|
|
224
|
+
const left = Math.max(0, Math.min(anchorCol, Math.max(0, columns - width)));
|
|
187
225
|
return (_jsx(Box, { position: "absolute", left: left, top: top, width: width, height: height, flexDirection: "column", flexShrink: 0, borderStyle: "round", borderColor: "permission", backgroundColor: "toolCardBackground", children: lines.map((line, index) => (_jsx(Text, { wrap: "truncate-end", children: line === '' ? ' ' : line }, index))) }));
|
|
188
226
|
}
|
|
189
227
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserPromptMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/UserPromptMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"UserPromptMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/UserPromptMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,eAAe,EAAE,OAAO,CAAA;IACxB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CAClC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,eAAe,EACf,UAAkB,EAClB,OAAO,GACR,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAiCzB"}
|
|
@@ -3,7 +3,6 @@ import { Box, Text, useTerminalSize } from '../../ui.js';
|
|
|
3
3
|
import { POINTER } from '../../terminal-utils/figures.js';
|
|
4
4
|
import { stringWidth } from '../../ink/stringWidth.js';
|
|
5
5
|
import { wrapWidth } from '../../sessions/format.js';
|
|
6
|
-
import { useTooltip } from '../Tooltip.js';
|
|
7
6
|
/**
|
|
8
7
|
* User prompt bubble: `❯ text` in bold userPromptLabel gold with no background
|
|
9
8
|
* fill (Kimi Code style: the user turn gets a distinct bold tint so it reads
|
|
@@ -21,10 +20,9 @@ export function UserPromptMessage({ text, marginTopOnTurn, isSelected = false, o
|
|
|
21
20
|
// Ink; a second wrap would move the continuation back to column zero.
|
|
22
21
|
const lines = wrapWidth(text, Math.max(1, columns - prefixWidth - 3));
|
|
23
22
|
const continuationIndent = ' '.repeat(prefixWidth);
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return (_jsx(Box, { flexDirection: "column", marginTop: marginTopOnTurn ? 1 : 0, backgroundColor: isSelected ? 'messageActionsBackground' : undefined, paddingRight: 1, onClick: onClick, ...(tooltipActive ? promptTooltip : {}), children: lines.map((line, index) => (_jsxs(Text, { color: "userPromptLabel", bold: true, wrap: "truncate-end", children: [index === 0 ? `${POINTER} ` : continuationIndent, line] }, index))) }));
|
|
23
|
+
// No hover tooltip here, deliberately: the message is pre-wrapped so every
|
|
24
|
+
// visual line is already on screen — a float would only repeat visible
|
|
25
|
+
// text, and worse, the card REPLACES the cells it covers, so a drag-copy
|
|
26
|
+
// crossing it yields the tooltip fragment instead of the message.
|
|
27
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: marginTopOnTurn ? 1 : 0, backgroundColor: isSelected ? 'messageActionsBackground' : undefined, paddingRight: 1, onClick: onClick, children: lines.map((line, index) => (_jsxs(Text, { color: "userPromptLabel", bold: true, wrap: "truncate-end", children: [index === 0 ? `${POINTER} ` : continuationIndent, line] }, index))) }));
|
|
30
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useImageInspection.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/useImageInspection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAA;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACxF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"useImageInspection.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/useImageInspection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAA;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACxF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAQ/D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAC1G,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM;;;aA4ErC,MAAM,KAAK,MAAM,KAAG,IAAI;kBAfI,SAAS;EAmBjD"}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ImageInspectionSession, inspectionRegion } from './imageInspection.js';
|
|
3
3
|
import { loadTranscriptImageFull } from './TranscriptImages.js';
|
|
4
|
+
// The attachment is identified by `image.id` (a content hash), never by the
|
|
5
|
+
// facade object: callers may hand a fresh facade for the same attachment on
|
|
6
|
+
// every render, and keying effects or memos on the object would then restart
|
|
7
|
+
// the session and re-request pixels each commit — a render loop (#885).
|
|
4
8
|
export function useImageInspection(image, available, columns, rows, cell, zoom) {
|
|
9
|
+
const imageId = image.id;
|
|
10
|
+
// Committed facade for the loaders below. Written in a layout effect, not
|
|
11
|
+
// during render: an interrupted render must not leak its facade into a
|
|
12
|
+
// timer that an earlier commit already scheduled.
|
|
13
|
+
const latestImage = React.useRef(image);
|
|
14
|
+
React.useLayoutEffect(() => { latestImage.current = image; }, [image]);
|
|
5
15
|
const session = React.useRef(null);
|
|
6
16
|
const [position, setPosition] = React.useState(null);
|
|
7
|
-
const center = position?.
|
|
17
|
+
const center = position?.imageId === imageId ? position.center : undefined;
|
|
8
18
|
const dimensions = React.useRef(null);
|
|
9
19
|
React.useEffect(() => {
|
|
10
|
-
const current = new ImageInspectionSession(
|
|
20
|
+
const current = new ImageInspectionSession(latestImage.current);
|
|
11
21
|
session.current = current;
|
|
12
22
|
dimensions.current = null;
|
|
13
23
|
setPosition(null);
|
|
14
24
|
return () => { current.dispose(); session.current = null; };
|
|
15
|
-
}, [
|
|
16
|
-
const request = React.useMemo(() => ({
|
|
25
|
+
}, [imageId]);
|
|
26
|
+
const request = React.useMemo(() => ({ imageId, available, columns, rows, cell, zoom, center }), [imageId, available, columns, rows, cell, zoom, center]);
|
|
17
27
|
const [result, setResult] = React.useState(null);
|
|
18
28
|
React.useEffect(() => {
|
|
19
29
|
if (!available || (zoom > 0 && !cell))
|
|
@@ -25,7 +35,7 @@ export function useImageInspection(image, available, columns, rows, cell, zoom)
|
|
|
25
35
|
const timer = setTimeout(() => {
|
|
26
36
|
void (async () => {
|
|
27
37
|
if (zoom === 0)
|
|
28
|
-
return loadTranscriptImageFull(
|
|
38
|
+
return loadTranscriptImageFull(latestImage.current, controller.signal);
|
|
29
39
|
const size = await current.metadata();
|
|
30
40
|
controller.signal.throwIfAborted();
|
|
31
41
|
dimensions.current = size;
|
|
@@ -53,9 +63,9 @@ export function useImageInspection(image, available, columns, rows, cell, zoom)
|
|
|
53
63
|
return;
|
|
54
64
|
const next = clampedCenter({ x: previous.x + x, y: previous.y + y });
|
|
55
65
|
if (next)
|
|
56
|
-
setPosition(old => old?.
|
|
57
|
-
? old : {
|
|
58
|
-
}, [clampedCenter,
|
|
66
|
+
setPosition(old => old?.imageId === imageId && old.center.x === next.x && old.center.y === next.y
|
|
67
|
+
? old : { imageId, center: next });
|
|
68
|
+
}, [clampedCenter, imageId]);
|
|
59
69
|
const dragOrigin = React.useRef(undefined);
|
|
60
70
|
const drag = React.useCallback((event) => {
|
|
61
71
|
event.stopImmediatePropagation();
|
|
@@ -70,7 +80,7 @@ export function useImageInspection(image, available, columns, rows, cell, zoom)
|
|
|
70
80
|
}, [cell, zoom, clampedCenter, move]);
|
|
71
81
|
// Preserve the previous pixels while panning, but never stretch stale pixels
|
|
72
82
|
// across a new geometry, zoom, terminal cell size, or attachment.
|
|
73
|
-
const reusable = result?.request.
|
|
83
|
+
const reusable = result?.request.imageId === imageId && result.request.zoom === zoom &&
|
|
74
84
|
result.request.columns === columns && result.request.rows === rows && result.request.cell === cell;
|
|
75
85
|
return {
|
|
76
86
|
source: available && reusable ? result?.source : undefined,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer draft that a screen hands to its owner on the way out.
|
|
3
|
+
*
|
|
4
|
+
* Every screen that REPLACES the conversation (`/resume`'s session screen, the
|
|
5
|
+
* session tree, `/settings`, the jobs panel, the trajectory scene) is an early
|
|
6
|
+
* return in `Chat`, so `PromptInput` is unmounted while the user is away and
|
|
7
|
+
* its text, caret and staged-image bindings live in component state. The owner
|
|
8
|
+
* therefore holds one slot of this type and the composer fills it as it
|
|
9
|
+
* unmounts; on the way back it is read, checked and consumed.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free (it does not import `PromptInput`, which would
|
|
12
|
+
* be a cycle), so the component and the headless regressions can both assert
|
|
13
|
+
* against the same rules.
|
|
14
|
+
*
|
|
15
|
+
* The snapshot shape, the image-liveness rule and the carried edit state
|
|
16
|
+
* (fold block, fullscreen editor, vim mode) are taken from PR #847
|
|
17
|
+
* (`promptDraftCache.ts`, branch `fix/composer-draft-retention`); the owner
|
|
18
|
+
* check here adds the agent id, because a generation alone cannot tell two
|
|
19
|
+
* channels with a fixed generation apart.
|
|
20
|
+
*/
|
|
21
|
+
/** One token → staged-image capability pair, in visible order. */
|
|
22
|
+
export type PromptDraftImage = readonly [token: string, stageId: string];
|
|
23
|
+
/**
|
|
24
|
+
* Everything the composer needs to put a draft back exactly as it was left.
|
|
25
|
+
*
|
|
26
|
+
* Transient interaction state — mouse selection, drag anchor, suggestion menu,
|
|
27
|
+
* history position, editor scroll, the vim undo stack — is NOT here: it
|
|
28
|
+
* describes a composer that was on screen, not the draft that outlives it.
|
|
29
|
+
*/
|
|
30
|
+
export interface PromptDraftSnapshot {
|
|
31
|
+
/** Agent the draft was typed into; a different one means "not this draft". */
|
|
32
|
+
readonly ownerAgentId: string;
|
|
33
|
+
/** Binding generation at capture time; see {@link resolveBindingGeneration}. */
|
|
34
|
+
readonly bindingGeneration: number;
|
|
35
|
+
readonly value: string;
|
|
36
|
+
readonly cursor: number;
|
|
37
|
+
/**
|
|
38
|
+
* Fold block [start, end) rendering as a one-line chip, or null. Edit
|
|
39
|
+
* state, not transient chrome: coming back with the text unfolded loses
|
|
40
|
+
* exactly what the user folded.
|
|
41
|
+
*/
|
|
42
|
+
readonly foldBlock: {
|
|
43
|
+
readonly start: number;
|
|
44
|
+
readonly end: number;
|
|
45
|
+
} | null;
|
|
46
|
+
/** Fullscreen draft editor open at capture time. */
|
|
47
|
+
readonly expanded: boolean;
|
|
48
|
+
/** vim mode on (`/vim`), and whether it is in INSERT (vs NORMAL) submode. */
|
|
49
|
+
readonly vimEnabled: boolean;
|
|
50
|
+
readonly vimInsert: boolean;
|
|
51
|
+
/** Visible `[Image #N]` token → staged stageId, so images survive the trip. */
|
|
52
|
+
readonly images: readonly PromptDraftImage[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The single slot an owner (today: `Chat`) keeps for the composer draft.
|
|
56
|
+
*
|
|
57
|
+
* A mutable ref-shaped cell rather than React state: it is written from an
|
|
58
|
+
* unmount cleanup and read from a mount effect, and neither may trigger a
|
|
59
|
+
* render — a draft that changed the first frame would move the transcript's
|
|
60
|
+
* restored scroll position.
|
|
61
|
+
*/
|
|
62
|
+
export interface PromptDraftCache {
|
|
63
|
+
current: PromptDraftSnapshot | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The current binding generation, mirroring the channel expression: the agent
|
|
67
|
+
* binding generation advances on every agent replacement (`/resume` to another
|
|
68
|
+
* session, `/new`, `/bg`, attach, rewind) and wins; a simplified or embedded
|
|
69
|
+
* channel without it falls back to the staged-image generation, and 0 when it
|
|
70
|
+
* has neither.
|
|
71
|
+
* @param source - The channel, or anything carrying the same two members.
|
|
72
|
+
* @returns The generation the snapshot is fenced against.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveBindingGeneration(source: {
|
|
75
|
+
readonly agentBindingGeneration?: number;
|
|
76
|
+
readonly stagedImageGeneration?: () => number;
|
|
77
|
+
}): number;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a stored draft belongs to the conversation in front of us now.
|
|
80
|
+
*
|
|
81
|
+
* Both halves are required. The generation catches a replaced binding; the
|
|
82
|
+
* agent id catches the case a generation cannot describe — a test or embedded
|
|
83
|
+
* channel whose generation is a constant, where a stale draft would otherwise
|
|
84
|
+
* be adopted by a different conversation.
|
|
85
|
+
* @param snapshot - The stored draft, if any.
|
|
86
|
+
* @param ownerAgentId - Agent id of the composer that would restore it.
|
|
87
|
+
* @param generation - Current binding generation.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isUsableDraftSnapshot(snapshot: PromptDraftSnapshot | null | undefined, ownerAgentId: string, generation: number): snapshot is PromptDraftSnapshot;
|
|
90
|
+
/**
|
|
91
|
+
* Drop the image bindings whose capability is gone.
|
|
92
|
+
*
|
|
93
|
+
* A visible `[Image #N]` is only a label; what makes it an attachment is the
|
|
94
|
+
* stageId behind it. A session switch (or a discard) revokes the capability and
|
|
95
|
+
* the label must not be re-bound by reading the number back out of the text.
|
|
96
|
+
* Entries that fail the check are dropped silently and the caller keeps the
|
|
97
|
+
* visible token as ordinary text, which is the behaviour an edit already has.
|
|
98
|
+
* @param images - Stored pairs.
|
|
99
|
+
* @param hasStagedImage - Channel capability probe; absent means "none live".
|
|
100
|
+
*/
|
|
101
|
+
export declare function filterLiveImageBindings(images: readonly PromptDraftImage[] | null | undefined, hasStagedImage?: (stageId: string) => boolean): readonly PromptDraftImage[];
|
|
102
|
+
//# sourceMappingURL=promptDraftCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promptDraftCache.d.ts","sourceRoot":"","sources":["../../../src/components/promptDraftCache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GAAG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;AAExE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC3E,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAC7C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAA;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAA;IACxC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAA;CAC9C,GAAG,MAAM,CAET;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,EAChD,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,QAAQ,IAAI,mBAAmB,CAKjC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,GAAG,SAAS,EACtD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAC5C,SAAS,gBAAgB,EAAE,CAW7B"}
|