@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.100
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +364 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1238 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1650 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +550 -0
- package/src/integrations/components/share-dialog-store.ts +55 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2018 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
appendEntry,
|
|
4
|
+
createBranch,
|
|
5
|
+
historyFromLinear,
|
|
6
|
+
labelTip,
|
|
7
|
+
navigateTo,
|
|
8
|
+
switchBranch,
|
|
9
|
+
tipOf,
|
|
10
|
+
type SessionHistory,
|
|
11
|
+
} from "@cueloop/schema";
|
|
12
|
+
import { entryTarget, treeRows } from "./tree-view";
|
|
13
|
+
|
|
14
|
+
const AT = "2026-09-01T10:00:00.000Z";
|
|
15
|
+
const LATER = "2026-09-01T11:00:00.000Z";
|
|
16
|
+
|
|
17
|
+
/** rev1 -> comment a1 on main; a branch "alt" off a1 with its own comment; main continues with rev2. */
|
|
18
|
+
function forked(): SessionHistory {
|
|
19
|
+
let history = historyFromLinear({
|
|
20
|
+
id: "ses_1",
|
|
21
|
+
revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
|
|
22
|
+
annotations: [],
|
|
23
|
+
message: null,
|
|
24
|
+
createdAt: AT,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
|
|
28
|
+
history = labelTip(history, "start");
|
|
29
|
+
history = createBranch(history, "alt");
|
|
30
|
+
history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
|
|
31
|
+
history = switchBranch(history, "main");
|
|
32
|
+
history = appendEntry(history, {
|
|
33
|
+
type: "revision",
|
|
34
|
+
by: "agent",
|
|
35
|
+
content: "v2",
|
|
36
|
+
createdAt: LATER,
|
|
37
|
+
}).history;
|
|
38
|
+
|
|
39
|
+
return history;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("treeRows", () => {
|
|
43
|
+
test("the current branch is the trunk; other segments indent under the entry they grew from", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const rows = treeRows(forked());
|
|
46
|
+
|
|
47
|
+
// Assert: rev1, a1 (labelled, alt hangs off it), the alt comment indented, then rev2 as main's tip
|
|
48
|
+
expect(rows.map((row) => [row.depth, row.text, row.onPath])).toEqual([
|
|
49
|
+
[0, "revision 1", true],
|
|
50
|
+
[0, "comment", true],
|
|
51
|
+
[1, "comment", false],
|
|
52
|
+
[0, "revision 2", true],
|
|
53
|
+
]);
|
|
54
|
+
expect(rows[1]!.label).toBe("start");
|
|
55
|
+
expect(rows[2]!.tips).toEqual(["alt"]);
|
|
56
|
+
expect(rows[3]!.tips).toEqual(["main"]);
|
|
57
|
+
expect(rows[3]!.isCurrentTip).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("on the other branch the trunk follows it and main's continuation indents", () => {
|
|
61
|
+
// Act
|
|
62
|
+
const rows = treeRows(switchBranch(forked(), "alt"));
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(rows.map((row) => [row.depth, row.text, row.onPath])).toEqual([
|
|
66
|
+
[0, "revision 1", true],
|
|
67
|
+
[0, "comment", true],
|
|
68
|
+
[1, "revision 2", false],
|
|
69
|
+
[0, "comment", true],
|
|
70
|
+
]);
|
|
71
|
+
expect(rows[3]!.isCurrentTip).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a branch summary reads as a return with its text", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const history = navigateTo(forked(), forked().entries[0]!.id, { summary: "too early" });
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const rows = treeRows(history);
|
|
80
|
+
|
|
81
|
+
// Assert: the summary hangs under rev1 as the trunk, the abandoned segment indents
|
|
82
|
+
expect(rows.find((row) => row.glyph === "↩")?.text).toBe('"too early"');
|
|
83
|
+
expect(rows.find((row) => row.glyph === "↩")?.isCurrentTip).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("entryTarget", () => {
|
|
88
|
+
test("the tip is here, another tip is a switch, an ancestor is a navigate on its branch", () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const history = forked();
|
|
91
|
+
const [rev1, a1, altComment, rev2] = history.entries;
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(entryTarget(history, rev2!.id)).toEqual({ kind: "here" });
|
|
95
|
+
expect(entryTarget(history, altComment!.id)).toEqual({ kind: "switch", branch: "alt" });
|
|
96
|
+
expect(entryTarget(history, a1!.id)).toEqual({
|
|
97
|
+
kind: "navigate",
|
|
98
|
+
entryId: a1!.id,
|
|
99
|
+
branch: "main",
|
|
100
|
+
});
|
|
101
|
+
expect(entryTarget(history, rev1!.id)?.kind).toBe("navigate");
|
|
102
|
+
expect(tipOf(history)).toBe(rev2!.id);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("an entry only another branch reaches is a navigate on that branch", () => {
|
|
106
|
+
// Arrange: alt gets a second comment so its first is no longer a tip
|
|
107
|
+
let history = switchBranch(forked(), "alt");
|
|
108
|
+
|
|
109
|
+
history = appendEntry(history, {
|
|
110
|
+
type: "comment",
|
|
111
|
+
annotationId: "b2",
|
|
112
|
+
createdAt: LATER,
|
|
113
|
+
}).history;
|
|
114
|
+
const altFirst = history.entries.find(
|
|
115
|
+
(entry) => entry.type === "comment" && entry.annotationId === "b1",
|
|
116
|
+
)!;
|
|
117
|
+
|
|
118
|
+
history = switchBranch(history, "main");
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
expect(entryTarget(history, altFirst.id)).toEqual({
|
|
122
|
+
kind: "navigate",
|
|
123
|
+
entryId: altFirst.id,
|
|
124
|
+
branch: "alt",
|
|
125
|
+
});
|
|
126
|
+
expect(entryTarget(history, "e_nope")).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session tree as rows for the rail's Tree tab. The trunk is the current
|
|
3
|
+
* branch's path (falling back to main), drawn at depth 0; every other segment
|
|
4
|
+
* indents under the entry it grew from. Rows know whether they sit on the
|
|
5
|
+
* active path, which branch tips rest on them, and the checkpoint name they
|
|
6
|
+
* carry, so the pane only paints. Pure: no controller, no daemon.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
MAIN_BRANCH,
|
|
11
|
+
pathOf,
|
|
12
|
+
tipOf,
|
|
13
|
+
type SessionEntry,
|
|
14
|
+
type SessionHistory,
|
|
15
|
+
} from "@cueloop/schema";
|
|
16
|
+
|
|
17
|
+
export interface TreeRow {
|
|
18
|
+
entryId: string;
|
|
19
|
+
depth: number;
|
|
20
|
+
glyph: string;
|
|
21
|
+
text: string;
|
|
22
|
+
onPath: boolean;
|
|
23
|
+
tips: string[];
|
|
24
|
+
isCurrentTip: boolean;
|
|
25
|
+
label?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What going to an entry means from where the reviewer stands. */
|
|
29
|
+
export type EntryTarget =
|
|
30
|
+
| { kind: "here" }
|
|
31
|
+
| { kind: "switch"; branch: string }
|
|
32
|
+
| { kind: "navigate"; entryId: string; branch: string };
|
|
33
|
+
|
|
34
|
+
/** The one-glyph, few-words reading of an entry a row shows. */
|
|
35
|
+
export interface EntryDescription {
|
|
36
|
+
glyph: string;
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The entry's kind, said briefly; agent revisions count their round along the path. */
|
|
41
|
+
export function describeEntry(entry: SessionEntry, round?: number): EntryDescription {
|
|
42
|
+
switch (entry.type) {
|
|
43
|
+
case "revision":
|
|
44
|
+
return entry.by === "agent"
|
|
45
|
+
? { glyph: "◉", text: round === undefined ? "revision" : `revision ${round}` }
|
|
46
|
+
: { glyph: "○", text: "edit" };
|
|
47
|
+
case "comment":
|
|
48
|
+
return { glyph: "·", text: "comment" };
|
|
49
|
+
case "comment-removed":
|
|
50
|
+
return { glyph: "·", text: "comment removed" };
|
|
51
|
+
case "message":
|
|
52
|
+
return {
|
|
53
|
+
glyph:
|
|
54
|
+
entry.message.outcome === "approved"
|
|
55
|
+
? "✓"
|
|
56
|
+
: entry.message.outcome === "changes_requested"
|
|
57
|
+
? "✗"
|
|
58
|
+
: "·",
|
|
59
|
+
text: entry.message.outcome.replace("_", " "),
|
|
60
|
+
};
|
|
61
|
+
case "branch-summary":
|
|
62
|
+
return { glyph: "↩", text: entry.text ? `"${entry.text}"` : "returned" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Rows in drawing order: trunk first at each fork, older segments before newer. */
|
|
67
|
+
export function treeRows(history: SessionHistory): TreeRow[] {
|
|
68
|
+
const children = new Map<string | null, SessionEntry[]>();
|
|
69
|
+
|
|
70
|
+
for (const entry of history.entries) {
|
|
71
|
+
const siblings = children.get(entry.parentId) ?? [];
|
|
72
|
+
|
|
73
|
+
siblings.push(entry);
|
|
74
|
+
children.set(entry.parentId, siblings);
|
|
75
|
+
}
|
|
76
|
+
for (const siblings of children.values()) {
|
|
77
|
+
siblings.sort(
|
|
78
|
+
(left, right) =>
|
|
79
|
+
left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const onPath = new Set(pathOf(history).map((entry) => entry.id));
|
|
83
|
+
const onMain = new Set(pathOf(history, tipOf(history, MAIN_BRANCH)).map((entry) => entry.id));
|
|
84
|
+
const tipsByEntry = new Map<string, string[]>();
|
|
85
|
+
|
|
86
|
+
for (const [branch, tip] of Object.entries(history.tips)) {
|
|
87
|
+
tipsByEntry.set(tip, [...(tipsByEntry.get(tip) ?? []), branch]);
|
|
88
|
+
}
|
|
89
|
+
const currentTip = tipOf(history);
|
|
90
|
+
const rows: TreeRow[] = [];
|
|
91
|
+
// the trunk continues through the child on the active path, else on main, else the oldest
|
|
92
|
+
const trunkChild = (siblings: SessionEntry[]): SessionEntry =>
|
|
93
|
+
siblings.find((entry) => onPath.has(entry.id)) ??
|
|
94
|
+
siblings.find((entry) => onMain.has(entry.id)) ??
|
|
95
|
+
siblings[0]!;
|
|
96
|
+
const walk = (entry: SessionEntry, depth: number, round: number): void => {
|
|
97
|
+
const nextRound = entry.type === "revision" && entry.by === "agent" ? round + 1 : round;
|
|
98
|
+
const described = describeEntry(
|
|
99
|
+
entry,
|
|
100
|
+
entry.type === "revision" && entry.by === "agent" ? nextRound : undefined,
|
|
101
|
+
);
|
|
102
|
+
const label = history.labels[entry.id];
|
|
103
|
+
const row: TreeRow = {
|
|
104
|
+
entryId: entry.id,
|
|
105
|
+
depth,
|
|
106
|
+
glyph: described.glyph,
|
|
107
|
+
text: described.text,
|
|
108
|
+
onPath: onPath.has(entry.id),
|
|
109
|
+
tips: tipsByEntry.get(entry.id) ?? [],
|
|
110
|
+
isCurrentTip: entry.id === currentTip,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (label !== undefined) row.label = label;
|
|
114
|
+
rows.push(row);
|
|
115
|
+
const siblings = children.get(entry.id) ?? [];
|
|
116
|
+
|
|
117
|
+
if (siblings.length === 0) return;
|
|
118
|
+
const trunk = trunkChild(siblings);
|
|
119
|
+
|
|
120
|
+
for (const child of siblings) if (child !== trunk) walk(child, depth + 1, nextRound);
|
|
121
|
+
walk(trunk, depth, nextRound);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
for (const root of children.get(null) ?? []) walk(root, 0, 0);
|
|
125
|
+
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Going to an entry: nothing when it is the current tip; a switch when it is
|
|
131
|
+
* another branch's tip; otherwise a navigate on the branch whose path holds
|
|
132
|
+
* it - the current branch when it can, else the first branch that reaches it.
|
|
133
|
+
*/
|
|
134
|
+
export function entryTarget(history: SessionHistory, entryId: string): EntryTarget | null {
|
|
135
|
+
if (entryId === tipOf(history)) return { kind: "here" };
|
|
136
|
+
const tipBranch = Object.entries(history.tips).find(([, tip]) => tip === entryId);
|
|
137
|
+
|
|
138
|
+
if (tipBranch) return { kind: "switch", branch: tipBranch[0] };
|
|
139
|
+
const holds = (branch: string): boolean =>
|
|
140
|
+
pathOf(history, tipOf(history, branch)).some((entry) => entry.id === entryId);
|
|
141
|
+
const branch = holds(history.branch)
|
|
142
|
+
? history.branch
|
|
143
|
+
: Object.keys(history.tips).find((candidate) => holds(candidate));
|
|
144
|
+
|
|
145
|
+
return branch === undefined ? null : { kind: "navigate", entryId, branch };
|
|
146
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
5
|
+
import { useThreadAgent, type ThreadAgentClient } from "./use-thread-agent";
|
|
6
|
+
import { waitForText, settle } from "../testing/test-support";
|
|
7
|
+
|
|
8
|
+
const initial: ThreadAgentState = {
|
|
9
|
+
threadId: "test",
|
|
10
|
+
phase: { kind: "idle" },
|
|
11
|
+
messages: [],
|
|
12
|
+
tools: [],
|
|
13
|
+
comments: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test("late configuration and action responses cannot replace a newer accepted state", async () => {
|
|
17
|
+
let state = initial;
|
|
18
|
+
let configure: ((value: ThreadAgentState) => void) | undefined;
|
|
19
|
+
let first: ((value: ThreadAgentState) => void) | undefined;
|
|
20
|
+
let request = async () => false;
|
|
21
|
+
let calls = 0;
|
|
22
|
+
const client: ThreadAgentClient = {
|
|
23
|
+
agentGet: async () => state,
|
|
24
|
+
agentConfigure: async () =>
|
|
25
|
+
new Promise((resolve) => {
|
|
26
|
+
configure = resolve;
|
|
27
|
+
}),
|
|
28
|
+
agentPrompt: async () => {
|
|
29
|
+
if (++calls === 1)
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
first = resolve;
|
|
32
|
+
});
|
|
33
|
+
state = { ...initial, phase: { kind: "running" } };
|
|
34
|
+
|
|
35
|
+
return state;
|
|
36
|
+
},
|
|
37
|
+
agentCancel: async () => state,
|
|
38
|
+
agentComment: async () => state,
|
|
39
|
+
agentPermission: async () => state,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function TestAgent(): React.ReactNode {
|
|
43
|
+
const agent = useThreadAgent("test", undefined, client);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
request = () => agent.act((api) => api.agentPrompt({ id: "test", text: "Prompt" }));
|
|
47
|
+
}, [agent]);
|
|
48
|
+
|
|
49
|
+
return <text>{agent.state.phase.kind}</text>;
|
|
50
|
+
}
|
|
51
|
+
const setup = await testRender(<TestAgent />, { width: 40, height: 4 });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await waitForText(setup, "idle");
|
|
55
|
+
const pending = request();
|
|
56
|
+
|
|
57
|
+
await request();
|
|
58
|
+
await waitForText(setup, "running");
|
|
59
|
+
first!(initial);
|
|
60
|
+
await pending;
|
|
61
|
+
configure!(initial);
|
|
62
|
+
await settle(setup);
|
|
63
|
+
expect(setup.captureCharFrame()).toContain("running");
|
|
64
|
+
expect(setup.captureCharFrame()).not.toContain("idle");
|
|
65
|
+
} finally {
|
|
66
|
+
setup.renderer.destroy();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("production subscription shows daemon updates and reconnects without an injected client", async () => {
|
|
71
|
+
const { mkdtempSync, rmSync } = await import("node:fs");
|
|
72
|
+
const { tmpdir } = await import("node:os");
|
|
73
|
+
const { join } = await import("node:path");
|
|
74
|
+
const { DaemonServer } = await import("@cueloop/daemon");
|
|
75
|
+
const { DaemonClient } = await import("@cueloop/daemon/client");
|
|
76
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-ui-subscription-"));
|
|
77
|
+
const adapter = {
|
|
78
|
+
id: "ui-subscription",
|
|
79
|
+
label: "UI subscription",
|
|
80
|
+
connect({ onEvent }: Parameters<import("@cueloop/schema").AgentHarnessAdapter["connect"]>[0]) {
|
|
81
|
+
return {
|
|
82
|
+
start: async () => "ui-session",
|
|
83
|
+
prompt: async () => {
|
|
84
|
+
onEvent({ kind: "message" as const, id: "answer", text: "Updated answer" });
|
|
85
|
+
|
|
86
|
+
return { outcome: "completed" as const };
|
|
87
|
+
},
|
|
88
|
+
cancel() {},
|
|
89
|
+
permission() {},
|
|
90
|
+
close() {},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const options = { home, idleExitMs: 0, threadAgent: { enabled: true, adapter } };
|
|
95
|
+
let server = new DaemonServer(options);
|
|
96
|
+
|
|
97
|
+
server.start();
|
|
98
|
+
const client = await DaemonClient.connect({ home });
|
|
99
|
+
const thread = await client.sessionCreate(
|
|
100
|
+
{ repoRoot: home, branch: "main" },
|
|
101
|
+
{ type: "plan", content: "Watch this", meta: {} },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
function TestAgentSubscription(): React.ReactNode {
|
|
105
|
+
const agent = useThreadAgent(thread.id, home);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<text>
|
|
109
|
+
{agent.client ? "Connected " : "Connecting "}
|
|
110
|
+
{agent.state.messages.map((message) => message.text).join(" ")}
|
|
111
|
+
{agent.error}
|
|
112
|
+
</text>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const setup = await testRender(<TestAgentSubscription />, { width: 80, height: 4 });
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await waitForText(setup, "Connected");
|
|
119
|
+
await client.agentPrompt({ id: thread.id, text: "Question" });
|
|
120
|
+
await waitForText(setup, "Updated answer");
|
|
121
|
+
server.stop();
|
|
122
|
+
server = new DaemonServer(options);
|
|
123
|
+
server.start();
|
|
124
|
+
const resumed = await DaemonClient.connect({ home });
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await resumed.agentPrompt({ id: thread.id, text: "Continuation" });
|
|
128
|
+
} finally {
|
|
129
|
+
resumed.close();
|
|
130
|
+
}
|
|
131
|
+
await waitForText(setup, "Continuation");
|
|
132
|
+
expect(setup.captureCharFrame()).toContain("Updated answer");
|
|
133
|
+
} finally {
|
|
134
|
+
setup.renderer.destroy();
|
|
135
|
+
client.close();
|
|
136
|
+
server.stop();
|
|
137
|
+
rmSync(home, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("reconnecting during a running turn preserves state without a configuration error", async () => {
|
|
142
|
+
const { mkdtempSync, rmSync } = await import("node:fs");
|
|
143
|
+
const { tmpdir } = await import("node:os");
|
|
144
|
+
const { join } = await import("node:path");
|
|
145
|
+
const { DaemonServer } = await import("@cueloop/daemon");
|
|
146
|
+
const { DaemonClient } = await import("@cueloop/daemon/client");
|
|
147
|
+
const { createTestSdkHarness } = await import("../../../../test/helpers/sdk-harness");
|
|
148
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-ui-running-"));
|
|
149
|
+
const harness = createTestSdkHarness();
|
|
150
|
+
const server = new DaemonServer({
|
|
151
|
+
home,
|
|
152
|
+
idleExitMs: 0,
|
|
153
|
+
threadAgent: { enabled: true, adapter: harness.adapter },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.start();
|
|
157
|
+
const client = await DaemonClient.connect({ home });
|
|
158
|
+
const thread = await client.sessionCreate(
|
|
159
|
+
{ repoRoot: home, branch: "main" },
|
|
160
|
+
{ type: "plan", content: "Watch this", meta: {} },
|
|
161
|
+
);
|
|
162
|
+
let connection: InstanceType<typeof DaemonClient> | undefined;
|
|
163
|
+
let connections = 0;
|
|
164
|
+
|
|
165
|
+
function TestRunningSubscription(): React.ReactNode {
|
|
166
|
+
const agent = useThreadAgent(thread.id, home);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (agent.client instanceof DaemonClient && agent.client !== connection) {
|
|
170
|
+
connection = agent.client;
|
|
171
|
+
connections++;
|
|
172
|
+
}
|
|
173
|
+
}, [agent.client]);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<text>
|
|
177
|
+
{connections} {agent.state.phase.kind} {agent.error || "No error"}
|
|
178
|
+
</text>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const setup = await testRender(<TestRunningSubscription />, { width: 100, height: 4 });
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await waitForText(setup, "idle");
|
|
185
|
+
await client.agentPrompt({ id: thread.id, text: "Question" });
|
|
186
|
+
await waitForText(setup, "running");
|
|
187
|
+
expect((await client.agentConfigure({ id: thread.id })).phase.kind).toBe("running");
|
|
188
|
+
const rejected = await client
|
|
189
|
+
.agentConfigure({ id: thread.id, configId: "model", value: "other" })
|
|
190
|
+
.catch((error: Error) => error);
|
|
191
|
+
|
|
192
|
+
expect(rejected).toMatchObject({
|
|
193
|
+
code: "internal",
|
|
194
|
+
message: "Thread agent configuration waits for the current turn",
|
|
195
|
+
});
|
|
196
|
+
connection!.close();
|
|
197
|
+
await waitForText(setup, "2 running");
|
|
198
|
+
harness.complete();
|
|
199
|
+
await waitForText(setup, "2 idle");
|
|
200
|
+
expect(setup.captureCharFrame()).toContain("No error");
|
|
201
|
+
expect(setup.captureCharFrame()).not.toContain("configuration waits");
|
|
202
|
+
} finally {
|
|
203
|
+
setup.renderer.destroy();
|
|
204
|
+
client.close();
|
|
205
|
+
server.stop();
|
|
206
|
+
rmSync(home, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { subscribeThreadState, connectThreadObserver } from "@cueloop/daemon/thread-subscription";
|
|
3
|
+
import { DaemonClient } from "@cueloop/daemon/client";
|
|
4
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
5
|
+
|
|
6
|
+
/** Agent transport is injectable for frame tests; production uses the daemon socket. */
|
|
7
|
+
export type ThreadAgentClient = Pick<
|
|
8
|
+
DaemonClient,
|
|
9
|
+
"agentGet" | "agentPrompt" | "agentCancel" | "agentComment" | "agentPermission"
|
|
10
|
+
> &
|
|
11
|
+
Partial<Pick<DaemonClient, "agentConfigure">>;
|
|
12
|
+
|
|
13
|
+
/** Subscribe to durable agent state; unmount closes only this client connection. */
|
|
14
|
+
export function useThreadAgent(id: string, home?: string, injected?: ThreadAgentClient) {
|
|
15
|
+
const [client, setClient] = useState<ThreadAgentClient | null>(injected ?? null);
|
|
16
|
+
const [state, setState] = useState<ThreadAgentState>({
|
|
17
|
+
threadId: id,
|
|
18
|
+
phase: { kind: "idle" },
|
|
19
|
+
messages: [],
|
|
20
|
+
tools: [],
|
|
21
|
+
comments: [],
|
|
22
|
+
});
|
|
23
|
+
const revision = useRef(0);
|
|
24
|
+
const [error, setError] = useState("");
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
let connection: DaemonClient | undefined;
|
|
29
|
+
let unsubscribe: (() => void) | undefined;
|
|
30
|
+
|
|
31
|
+
revision.current++;
|
|
32
|
+
const refresh = async (api: ThreadAgentClient) => {
|
|
33
|
+
const requested = ++revision.current;
|
|
34
|
+
const result = await api.agentGet(id);
|
|
35
|
+
|
|
36
|
+
if (!cancelled && requested === revision.current) setState(result);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const configure = async (api: ThreadAgentClient): Promise<void> => {
|
|
40
|
+
if (!api.agentConfigure) return;
|
|
41
|
+
const requested = ++revision.current;
|
|
42
|
+
const configured = await api.agentConfigure({ id });
|
|
43
|
+
|
|
44
|
+
if (!cancelled && requested === revision.current) setState(configured);
|
|
45
|
+
await refresh(api);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
void (async () => {
|
|
49
|
+
try {
|
|
50
|
+
if (injected) {
|
|
51
|
+
await refresh(injected);
|
|
52
|
+
await configure(injected);
|
|
53
|
+
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
unsubscribe = subscribeThreadState({
|
|
57
|
+
connect: connectThreadObserver({ home, autostart: true }),
|
|
58
|
+
matches: (event) => event.event === "agent.updated" && event.sessionId === id,
|
|
59
|
+
read: async (api, signal) => {
|
|
60
|
+
const requested = ++revision.current;
|
|
61
|
+
const state = await api.agentGet(id, { signal });
|
|
62
|
+
|
|
63
|
+
return { state, requested };
|
|
64
|
+
},
|
|
65
|
+
onConnect: (api) => {
|
|
66
|
+
connection = api;
|
|
67
|
+
setClient(api);
|
|
68
|
+
void configure(api).catch((error) => {
|
|
69
|
+
if (!cancelled) setError(String(error));
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
onValue: ({ state, requested }) => {
|
|
73
|
+
if (!cancelled && requested === revision.current) setState(state);
|
|
74
|
+
},
|
|
75
|
+
onError: (error) => {
|
|
76
|
+
if (!cancelled) setError(String(error));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!cancelled) setError(error instanceof Error ? error.message : String(error));
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
unsubscribe?.();
|
|
87
|
+
connection?.close();
|
|
88
|
+
};
|
|
89
|
+
}, [id, home, injected]);
|
|
90
|
+
|
|
91
|
+
const act = async (
|
|
92
|
+
request: (client: ThreadAgentClient) => Promise<ThreadAgentState>,
|
|
93
|
+
): Promise<boolean> => {
|
|
94
|
+
if (!client) return false;
|
|
95
|
+
try {
|
|
96
|
+
const requested = ++revision.current;
|
|
97
|
+
const result = await request(client);
|
|
98
|
+
|
|
99
|
+
if (requested === revision.current) setState(result);
|
|
100
|
+
setError("");
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return { state, client, error, setError, act };
|
|
111
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../../appearance/theme";
|
|
3
|
+
import type { Story, StoryMeta } from "../../stories/story";
|
|
4
|
+
import { CodeBlock } from "./CodeBlock";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Primitives/CodeBlock" };
|
|
7
|
+
|
|
8
|
+
const SNIPPET = `export function gate(full: number) {
|
|
9
|
+
// threshold
|
|
10
|
+
return full < 0.6 ? "blocked" : "clear";
|
|
11
|
+
}`;
|
|
12
|
+
|
|
13
|
+
export const Typescript: Story = {
|
|
14
|
+
render: () => <CodeBlock language="ts" content={SNIPPET} />,
|
|
15
|
+
expectedColors: [DARK.elevated],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const CursorAnnotatedEdited: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<CodeBlock
|
|
21
|
+
language="ts"
|
|
22
|
+
content={SNIPPET}
|
|
23
|
+
isCursor
|
|
24
|
+
isAnnotated
|
|
25
|
+
changeTag="edited"
|
|
26
|
+
marginTop={1}
|
|
27
|
+
/>
|
|
28
|
+
),
|
|
29
|
+
expectedColors: [DARK.accent],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const UnknownLanguage: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<CodeBlock language="brainfog" content={"plain verbatim lines\n with indentation"} />
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** A cut code block: struck-through gray content, no [cut] tag. */
|
|
39
|
+
export const Cut: Story = {
|
|
40
|
+
render: () => <CodeBlock language="ts" content={SNIPPET} cut />,
|
|
41
|
+
expectedColors: [DARK.elevated],
|
|
42
|
+
};
|