@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The review-session controller (#70): every daemon round-trip and mutation
|
|
3
|
-
* verb behind one React-free object. It owns connect/autostart/subscribe,
|
|
4
|
-
* the session/inbox/status/error snapshot, optimistic apply, the mutation
|
|
5
|
-
* verbs (cut/edit/annotate/submit/...), and the post-submit completion
|
|
6
|
-
* lifecycle including the notes-vault export and the herdr return-focus.
|
|
7
|
-
* App subscribes to snapshots and keeps only view state.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { DaemonClient } from "@cueloop/daemon/client";
|
|
11
|
-
import {
|
|
12
|
-
cutBlock,
|
|
13
|
-
makeAnchor,
|
|
14
|
-
newAnnotationId,
|
|
15
|
-
parseBlocks,
|
|
16
|
-
resolveAnchor,
|
|
17
|
-
restoreBlock,
|
|
18
|
-
restoreLine,
|
|
19
|
-
type ReviewSession,
|
|
20
|
-
type VerdictKind,
|
|
21
|
-
} from "@cueloop/schema";
|
|
22
|
-
import { Registry, type Exporter } from "@cueloop/extension-api";
|
|
23
|
-
import { createObsidianExtension, shouldExport, type ObsidianConfig } from "@cueloop/integration-obsidian";
|
|
24
|
-
import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view";
|
|
25
|
-
import { diffRowAnchor, diffRows, type DiffRow } from "./view-diff";
|
|
26
|
-
import { editInEditor } from "./editor";
|
|
27
|
-
import { focusHerdrPane, returnPaneFor } from "./herdr";
|
|
28
|
-
import { persistAutoClose, type AutoClose, type CueloopConfig } from "./config";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Post-submit lifecycle (a review pane should hand you back to the agent,
|
|
32
|
-
* not linger): idle → prompt (auto-close off: offer the choice) or
|
|
33
|
-
* counting (configured delay) → exit. esc dismisses to the resolved view.
|
|
34
|
-
*/
|
|
35
|
-
export type Completion =
|
|
36
|
-
| { phase: "idle" }
|
|
37
|
-
| { phase: "prompt" }
|
|
38
|
-
| { phase: "counting"; remaining: number }
|
|
39
|
-
| { phase: "dismissed" };
|
|
40
|
-
|
|
41
|
-
export interface ControllerSnapshot {
|
|
42
|
-
session: ReviewSession | null;
|
|
43
|
-
inbox: ReviewSession[] | null;
|
|
44
|
-
status: string;
|
|
45
|
-
error: string | null;
|
|
46
|
-
completion: Completion;
|
|
47
|
-
/**
|
|
48
|
-
* Annotations whose anchor stopped resolving after the last editor
|
|
49
|
-
* hand-off - the reconciliation banner count. 0 = no banner.
|
|
50
|
-
*/
|
|
51
|
-
editOrphanCount: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ReviewControllerOptions {
|
|
55
|
-
home?: string;
|
|
56
|
-
sessionId?: string;
|
|
57
|
-
/** Observer mode: stored for the key reducer's read-only gate. */
|
|
58
|
-
readOnly?: boolean;
|
|
59
|
-
onExit?: (code: number) => void;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ReviewController {
|
|
63
|
-
readonly readOnly: boolean;
|
|
64
|
-
/** Snapshot listeners (stable identity - safe for useSyncExternalStore). */
|
|
65
|
-
subscribe(listener: () => void): () => void;
|
|
66
|
-
getSnapshot(): ControllerSnapshot;
|
|
67
|
-
/** Dial the daemon (autostart), subscribe to events, fetch session or inbox. */
|
|
68
|
-
connect(): void;
|
|
69
|
-
close(): void;
|
|
70
|
-
/** Loaded config parts the controller acts on: auto-close and exporters. */
|
|
71
|
-
applyConfig(cfg: CueloopConfig): void;
|
|
72
|
-
setStatus(msg: string): void;
|
|
73
|
-
/** Derived projections, cached per session identity. */
|
|
74
|
-
display(): DisplayBlock[];
|
|
75
|
-
rows(): DiffRow[];
|
|
76
|
-
working(): string;
|
|
77
|
-
/** Open a session from the inbox. */
|
|
78
|
-
open(id: string): void;
|
|
79
|
-
/** Cut the block under the cursor, or restore a cut one. */
|
|
80
|
-
cut(dispIdx: number): void;
|
|
81
|
-
/** The $EDITOR hand-off on the working copy. */
|
|
82
|
-
edit(): void;
|
|
83
|
-
/**
|
|
84
|
-
* Anchor and store an annotation; both plan and diff anchor constructions.
|
|
85
|
-
* Returns the minted annotation id so the view can select the new card.
|
|
86
|
-
*/
|
|
87
|
-
annotate(kind: "comment" | "suggestion", dispIdx: number, start: number, end: number, body: string): string | undefined;
|
|
88
|
-
/** Rewrite a stored annotation's body in place (the rail-card edit). */
|
|
89
|
-
updateAnnotation(id: string, body: string): void;
|
|
90
|
-
removeAnnotation(id: string): void;
|
|
91
|
-
setWorkingCopy(content: string | undefined): void;
|
|
92
|
-
/** Resolve the review, run the export, start the completion hand-back. */
|
|
93
|
-
submit(verdict: VerdictKind, summary: string): void;
|
|
94
|
-
/** Close the review and, inside herdr, bounce focus back to the agent. */
|
|
95
|
-
finishReview(): void;
|
|
96
|
-
dismissCompletion(): void;
|
|
97
|
-
/** From the completion prompt: persist auto-close and start the countdown. */
|
|
98
|
-
optInAutoClose(): void;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function createReviewController(opts: ReviewControllerOptions): ReviewController {
|
|
102
|
-
return new Controller(opts);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
class Controller implements ReviewController {
|
|
106
|
-
readonly readOnly: boolean;
|
|
107
|
-
private client: DaemonClient | null = null;
|
|
108
|
-
private closed = false;
|
|
109
|
-
private snap: ControllerSnapshot = {
|
|
110
|
-
session: null,
|
|
111
|
-
inbox: null,
|
|
112
|
-
status: "",
|
|
113
|
-
error: null,
|
|
114
|
-
completion: { phase: "idle" },
|
|
115
|
-
editOrphanCount: 0,
|
|
116
|
-
};
|
|
117
|
-
private listeners = new Set<() => void>();
|
|
118
|
-
private autoClose: AutoClose = "off";
|
|
119
|
-
private obsidian: ObsidianConfig | null = null;
|
|
120
|
-
private exporters = new Map<string, Exporter>();
|
|
121
|
-
private countdown: ReturnType<typeof setTimeout> | undefined;
|
|
122
|
-
/** Projections keyed by session identity so renders reuse one computation. */
|
|
123
|
-
private derivedFor: ReviewSession | null = null;
|
|
124
|
-
private derived: { display: DisplayBlock[]; rows: DiffRow[] } = { display: [], rows: [] };
|
|
125
|
-
|
|
126
|
-
constructor(private readonly opts: ReviewControllerOptions) {
|
|
127
|
-
this.readOnly = opts.readOnly ?? false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
subscribe = (listener: () => void): (() => void) => {
|
|
131
|
-
this.listeners.add(listener);
|
|
132
|
-
return () => this.listeners.delete(listener);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
getSnapshot = (): ControllerSnapshot => this.snap;
|
|
136
|
-
|
|
137
|
-
private update(patch: Partial<ControllerSnapshot>): void {
|
|
138
|
-
this.snap = { ...this.snap, ...patch };
|
|
139
|
-
for (const l of this.listeners) l();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
connect(): void {
|
|
143
|
-
void (async () => {
|
|
144
|
-
try {
|
|
145
|
-
const client = await DaemonClient.connect({ home: this.opts.home, autostart: true });
|
|
146
|
-
if (this.closed) return void client.close();
|
|
147
|
-
this.client = client;
|
|
148
|
-
client.onEvent((e) => {
|
|
149
|
-
// another controller/observer changed state: re-fetch
|
|
150
|
-
const s = this.snap.session;
|
|
151
|
-
if (s && e.sessionId === s.id) void this.refreshSession(s.id);
|
|
152
|
-
if (!this.opts.sessionId) void this.refreshInbox();
|
|
153
|
-
});
|
|
154
|
-
await client.subscribe();
|
|
155
|
-
if (this.opts.sessionId) {
|
|
156
|
-
this.update({ session: await client.sessionGet(this.opts.sessionId) });
|
|
157
|
-
} else {
|
|
158
|
-
this.update({ inbox: await client.sessionList({ status: "pending" }) });
|
|
159
|
-
}
|
|
160
|
-
} catch (err) {
|
|
161
|
-
this.update({ error: err instanceof Error ? err.message : String(err) });
|
|
162
|
-
}
|
|
163
|
-
})();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
close(): void {
|
|
167
|
-
this.closed = true;
|
|
168
|
-
clearTimeout(this.countdown);
|
|
169
|
-
this.client?.close();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
applyConfig(cfg: CueloopConfig): void {
|
|
173
|
-
this.autoClose = cfg.ui.autoClose;
|
|
174
|
-
this.obsidian = cfg.integrations.obsidian;
|
|
175
|
-
// bundled integrations register through the public extension API
|
|
176
|
-
const registry = new Registry();
|
|
177
|
-
void registry.load("obsidian", createObsidianExtension(cfg.integrations.obsidian)).then((rec) => {
|
|
178
|
-
this.exporters = rec.exporters;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
setStatus(msg: string): void {
|
|
183
|
-
this.update({ status: msg });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── derived projections ─────────────────────
|
|
187
|
-
private ensureDerived(): void {
|
|
188
|
-
const s = this.snap.session;
|
|
189
|
-
if (this.derivedFor === s) return;
|
|
190
|
-
this.derivedFor = s;
|
|
191
|
-
this.derived = {
|
|
192
|
-
display: s ? buildDisplay(s.artifact.content, s.workingCopy) : [],
|
|
193
|
-
rows: s && s.artifact.type === "diff" ? diffRows(s.artifact.content) : [],
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
display(): DisplayBlock[] {
|
|
198
|
-
this.ensureDerived();
|
|
199
|
-
return this.derived.display;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
rows(): DiffRow[] {
|
|
203
|
-
this.ensureDerived();
|
|
204
|
-
return this.derived.rows;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
working(): string {
|
|
208
|
-
const s = this.snap.session;
|
|
209
|
-
return s ? (s.workingCopy ?? s.artifact.content) : "";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async refreshSession(id: string): Promise<void> {
|
|
213
|
-
if (this.client) this.update({ session: await this.client.sessionGet(id) });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private async refreshInbox(): Promise<void> {
|
|
217
|
-
if (this.client) this.update({ inbox: await this.client.sessionList({ status: "pending" }) });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Optimistic apply: the daemon response is the next session snapshot. */
|
|
221
|
-
private apply(p: Promise<ReviewSession>): void {
|
|
222
|
-
p.then((session) => this.update({ session })).catch((e: unknown) =>
|
|
223
|
-
this.setStatus(String(e instanceof Error ? e.message : e)),
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── verbs ───────────────────────────────────
|
|
228
|
-
open(id: string): void {
|
|
229
|
-
const cached = this.snap.inbox?.find((s) => s.id === id);
|
|
230
|
-
if (cached) this.update({ session: cached });
|
|
231
|
-
else void this.refreshSession(id);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
cut(dispIdx: number): void {
|
|
235
|
-
const session = this.snap.session;
|
|
236
|
-
if (!session || session.status === "resolved") return;
|
|
237
|
-
const d = this.display()[dispIdx];
|
|
238
|
-
if (!d) return;
|
|
239
|
-
const working = this.working();
|
|
240
|
-
if (d.type === "del") {
|
|
241
|
-
const line = restoreLine(nextWorkBlock(this.display(), dispIdx), working.split("\n").length);
|
|
242
|
-
// restoreBlock returns undefined when the block structure round-trips
|
|
243
|
-
// to the submitted revision - the working copy is gone
|
|
244
|
-
const restored = restoreBlock(session.artifact.content, working, d.base!, line);
|
|
245
|
-
this.setWorkingCopy(restored);
|
|
246
|
-
this.setStatus("cut restored");
|
|
247
|
-
} else if (d.work) {
|
|
248
|
-
this.setWorkingCopy(cutBlock(working, d.work));
|
|
249
|
-
this.setStatus("block cut - it serializes into the diff");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
edit(): void {
|
|
254
|
-
const session = this.snap.session;
|
|
255
|
-
if (!session || session.status === "resolved") return;
|
|
256
|
-
try {
|
|
257
|
-
const result = editInEditor(this.working(), "plan.md");
|
|
258
|
-
if (result.changed) {
|
|
259
|
-
this.setWorkingCopy(result.content);
|
|
260
|
-
this.reconcileAnnotations(session, result.content);
|
|
261
|
-
this.setStatus("edits tracked - one diff");
|
|
262
|
-
} else this.setStatus("no changes");
|
|
263
|
-
} catch (err) {
|
|
264
|
-
this.setStatus(err instanceof Error ? err.message : String(err));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Edit-exit reconciliation: re-resolve every annotation against the edited
|
|
270
|
-
* working copy with the quote-primary cascade. Annotations that stop
|
|
271
|
-
* resolving stay stored (the feedback serializer flags orphaned anchors);
|
|
272
|
-
* the count feeds the one-line banner above the sheet.
|
|
273
|
-
*/
|
|
274
|
-
private reconcileAnnotations(session: ReviewSession, editedContent: string): void {
|
|
275
|
-
const editedBlocks = parseBlocks(editedContent);
|
|
276
|
-
const orphanCount = session.annotations.filter(
|
|
277
|
-
(annotation) => resolveAnchor(annotation.anchor, editedBlocks) === null,
|
|
278
|
-
).length;
|
|
279
|
-
this.update({ editOrphanCount: orphanCount });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
annotate(
|
|
283
|
-
kind: "comment" | "suggestion",
|
|
284
|
-
dispIdx: number,
|
|
285
|
-
start: number,
|
|
286
|
-
end: number,
|
|
287
|
-
body: string,
|
|
288
|
-
): string | undefined {
|
|
289
|
-
const session = this.snap.session;
|
|
290
|
-
if (!session) return undefined;
|
|
291
|
-
let anchor;
|
|
292
|
-
if (session.artifact.type === "diff") {
|
|
293
|
-
anchor = { ...diffRowAnchor(this.rows(), dispIdx), blockIndex: dispIdx };
|
|
294
|
-
} else {
|
|
295
|
-
const display = this.display();
|
|
296
|
-
const workBlocks = display.filter((d) => d.work).map((d) => d.work!);
|
|
297
|
-
const workIdx = display.slice(0, dispIdx + 1).filter((d) => d.work).length - 1;
|
|
298
|
-
anchor = makeAnchor(workBlocks, workIdx, start, end);
|
|
299
|
-
}
|
|
300
|
-
const annotationId = newAnnotationId();
|
|
301
|
-
this.apply(
|
|
302
|
-
this.client!.sessionAnnotate(session.id, {
|
|
303
|
-
id: annotationId,
|
|
304
|
-
kind,
|
|
305
|
-
anchor,
|
|
306
|
-
body,
|
|
307
|
-
}),
|
|
308
|
-
);
|
|
309
|
-
this.setStatus(kind === "suggestion" ? "suggestion added - the agent applies it" : "comment added");
|
|
310
|
-
return annotationId;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
updateAnnotation(id: string, body: string): void {
|
|
314
|
-
const session = this.snap.session;
|
|
315
|
-
if (!session) return;
|
|
316
|
-
const existing = session.annotations.find((annotation) => annotation.id === id);
|
|
317
|
-
if (!existing) return;
|
|
318
|
-
// the daemon's annotate verb upserts by id: same id + anchor, new body
|
|
319
|
-
this.apply(
|
|
320
|
-
this.client!.sessionAnnotate(session.id, {
|
|
321
|
-
id: existing.id,
|
|
322
|
-
kind: existing.kind,
|
|
323
|
-
anchor: existing.anchor,
|
|
324
|
-
body,
|
|
325
|
-
}),
|
|
326
|
-
);
|
|
327
|
-
this.setStatus("annotation updated");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
removeAnnotation(id: string): void {
|
|
331
|
-
const session = this.snap.session;
|
|
332
|
-
if (!session) return;
|
|
333
|
-
this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
|
|
334
|
-
this.setStatus("annotation deleted");
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
setWorkingCopy(content: string | undefined): void {
|
|
338
|
-
const session = this.snap.session;
|
|
339
|
-
if (!session) return;
|
|
340
|
-
this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
submit(verdict: VerdictKind, summary: string): void {
|
|
344
|
-
const session = this.snap.session;
|
|
345
|
-
if (!session) return;
|
|
346
|
-
this.client!.sessionResolve(session.id, verdict, summary)
|
|
347
|
-
.then((resolved) => {
|
|
348
|
-
this.update({ session: resolved, status: `review submitted - ${verdict.replace("_", " ")}` });
|
|
349
|
-
// notes-vault export: guarded by config, default is manual (no-op)
|
|
350
|
-
const obsidian = this.obsidian;
|
|
351
|
-
const exporter = this.exporters.get("obsidian");
|
|
352
|
-
if (obsidian && exporter && shouldExport(obsidian.exportOn, verdict)) {
|
|
353
|
-
void exporter(resolved).then((r) => {
|
|
354
|
-
this.setStatus(r.success && r.path ? `exported to ${r.path}` : `export failed: ${r.error ?? "unknown"}`);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
// hand the reviewer back to the agent: prompt or count down per config;
|
|
358
|
-
// inside herdr with a known return pane, closing is the default
|
|
359
|
-
const delay = this.autoClose;
|
|
360
|
-
const returns = returnPaneFor(resolved.artifact.meta.herdrPane) !== undefined;
|
|
361
|
-
if (delay === 0) this.finishReview();
|
|
362
|
-
else if (typeof delay === "number") this.startCounting(delay);
|
|
363
|
-
else if (returns) this.startCounting(3);
|
|
364
|
-
else this.update({ completion: { phase: "prompt" } });
|
|
365
|
-
})
|
|
366
|
-
.catch((e: unknown) => this.setStatus(String(e instanceof Error ? e.message : e)));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ── completion hand-back ────────────────────
|
|
370
|
-
private startCounting(remaining: number): void {
|
|
371
|
-
if (remaining <= 0) return this.finishReview();
|
|
372
|
-
this.update({ completion: { phase: "counting", remaining } });
|
|
373
|
-
this.countdown = setTimeout(() => this.startCounting(remaining - 1), 1000);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
finishReview(): void {
|
|
377
|
-
clearTimeout(this.countdown);
|
|
378
|
-
const pane = returnPaneFor(this.snap.session?.artifact.meta.herdrPane);
|
|
379
|
-
if (pane) focusHerdrPane(pane);
|
|
380
|
-
this.opts.onExit?.(0);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
dismissCompletion(): void {
|
|
384
|
-
clearTimeout(this.countdown);
|
|
385
|
-
this.update({ completion: { phase: "dismissed" } });
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
optInAutoClose(): void {
|
|
389
|
-
// opt in: close in 3s from now on, persisted to the user config
|
|
390
|
-
try {
|
|
391
|
-
persistAutoClose(3);
|
|
392
|
-
} catch {
|
|
393
|
-
// a read-only config dir must not block closing the review
|
|
394
|
-
}
|
|
395
|
-
this.autoClose = 3;
|
|
396
|
-
this.startCounting(3);
|
|
397
|
-
}
|
|
398
|
-
}
|
package/src/syntax.test.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { filetypeFor, highlightCode } from "./syntax";
|
|
3
|
-
|
|
4
|
-
const TS = `export function gate(full: number) {
|
|
5
|
-
// threshold
|
|
6
|
-
return full < 0.6 ? "blocked" : "clear";
|
|
7
|
-
}`;
|
|
8
|
-
|
|
9
|
-
describe("highlightCode", () => {
|
|
10
|
-
test("token text concatenates back to the exact source (verbatim rendering)", async () => {
|
|
11
|
-
const lines = await highlightCode(TS, "ts");
|
|
12
|
-
expect(lines).not.toBeNull();
|
|
13
|
-
const rebuilt = lines!.map((l) => l.map((t) => t.content).join("")).join("\n");
|
|
14
|
-
expect(rebuilt).toBe(TS);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("keywords, strings, and comments get distinct colors", async () => {
|
|
18
|
-
const lines = await highlightCode(TS, "ts");
|
|
19
|
-
const colorOf = (needle: string) => {
|
|
20
|
-
for (const line of lines!) for (const t of line) if (t.content.includes(needle)) return t.color;
|
|
21
|
-
return undefined;
|
|
22
|
-
};
|
|
23
|
-
const kw = colorOf("export");
|
|
24
|
-
const str = colorOf("blocked");
|
|
25
|
-
const comment = colorOf("threshold");
|
|
26
|
-
expect(kw).toBeDefined();
|
|
27
|
-
expect(str).toBeDefined();
|
|
28
|
-
expect(comment).toBeDefined();
|
|
29
|
-
expect(new Set([kw, str, comment]).size).toBe(3);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("unknown languages return null and render unstyled", async () => {
|
|
33
|
-
expect(await highlightCode("whatever", "brainfog")).toBeNull();
|
|
34
|
-
expect(await highlightCode("plain", undefined)).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("fence aliases map to real grammars", async () => {
|
|
38
|
-
expect(await highlightCode("x = 1", "py")).not.toBeNull();
|
|
39
|
-
expect(await highlightCode('echo "hi"', "sh")).not.toBeNull();
|
|
40
|
-
});
|
|
41
|
-
});
|
package/src/syntax.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code-block syntax highlighting via Shiki (#67): TextMate-grammar tokens
|
|
3
|
-
* colored by a theme built from cueloop's tokens. The highlighter loads
|
|
4
|
-
* lazily on the first code block; unknown languages render unstyled.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { DARK as T } from "./theme";
|
|
8
|
-
|
|
9
|
-
export interface CodeToken {
|
|
10
|
-
content: string;
|
|
11
|
-
color?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Highlighter = {
|
|
15
|
-
codeToTokensBase(code: string, opts: { lang: string; theme: string }): { content: string; color?: string }[][];
|
|
16
|
-
getLoadedLanguages(): string[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const LANGS = ["typescript", "tsx", "javascript", "jsx", "json", "bash", "python", "rust", "go", "yaml", "diff", "markdown", "toml", "sql", "html", "css"];
|
|
20
|
-
|
|
21
|
-
/** Markdown fence info → Shiki language id. */
|
|
22
|
-
export function filetypeFor(lang?: string): string {
|
|
23
|
-
const l = (lang ?? "").toLowerCase();
|
|
24
|
-
const map: Record<string, string> = {
|
|
25
|
-
ts: "typescript",
|
|
26
|
-
js: "javascript",
|
|
27
|
-
py: "python",
|
|
28
|
-
rb: "ruby",
|
|
29
|
-
rs: "rust",
|
|
30
|
-
sh: "bash",
|
|
31
|
-
shell: "bash",
|
|
32
|
-
zsh: "bash",
|
|
33
|
-
yml: "yaml",
|
|
34
|
-
};
|
|
35
|
-
return map[l] ?? l;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function cueloopTheme() {
|
|
39
|
-
const scope = (scopes: string, foreground: string, fontStyle?: string) => ({
|
|
40
|
-
scope: scopes.split(", "),
|
|
41
|
-
settings: fontStyle ? { foreground, fontStyle } : { foreground },
|
|
42
|
-
});
|
|
43
|
-
return {
|
|
44
|
-
name: "cueloop",
|
|
45
|
-
type: "dark" as const,
|
|
46
|
-
colors: { "editor.background": T.elevated, "editor.foreground": T.textMuted },
|
|
47
|
-
settings: [
|
|
48
|
-
{ settings: { foreground: T.textMuted } },
|
|
49
|
-
scope("comment, punctuation.definition.comment", T.textDim, "italic"),
|
|
50
|
-
scope("string, string.quoted, punctuation.definition.string", T.green),
|
|
51
|
-
scope("constant.numeric, constant.language, constant.character", T.blue),
|
|
52
|
-
scope("keyword, storage, storage.type, storage.modifier, keyword.control", T.accent),
|
|
53
|
-
scope("entity.name.function, support.function, meta.function-call", T.text),
|
|
54
|
-
scope("entity.name.type, entity.name.class, support.type, support.class", T.blue),
|
|
55
|
-
scope("variable, variable.other, variable.parameter", T.textMuted),
|
|
56
|
-
scope("entity.name.tag", T.accent),
|
|
57
|
-
scope("entity.other.attribute-name", T.blue),
|
|
58
|
-
scope("punctuation, meta.brace", T.textDim),
|
|
59
|
-
scope("keyword.operator", T.textDim),
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let highlighterPromise: Promise<Highlighter | null> | null = null;
|
|
65
|
-
|
|
66
|
-
function getHighlighter(): Promise<Highlighter | null> {
|
|
67
|
-
highlighterPromise ??= import("shiki")
|
|
68
|
-
.then((shiki) => shiki.createHighlighter({ themes: [cueloopTheme()], langs: LANGS }) as Promise<Highlighter>)
|
|
69
|
-
.catch(() => null); // highlighting is an enhancement, never a blocker
|
|
70
|
-
return highlighterPromise;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Tokenize code into styled lines. Token text concatenates back to the exact
|
|
75
|
-
* source line, so verbatim rendering (indentation, no wrap) is preserved.
|
|
76
|
-
* Returns null when the language is unknown or the highlighter is unavailable.
|
|
77
|
-
*/
|
|
78
|
-
export async function highlightCode(content: string, lang?: string): Promise<CodeToken[][] | null> {
|
|
79
|
-
const language = filetypeFor(lang);
|
|
80
|
-
if (!language) return null;
|
|
81
|
-
const highlighter = await getHighlighter();
|
|
82
|
-
if (!highlighter || !highlighter.getLoadedLanguages().includes(language)) return null;
|
|
83
|
-
try {
|
|
84
|
-
return highlighter.codeToTokensBase(content, { lang: language, theme: "cueloop" });
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
package/src/view.test.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
blockRuns,
|
|
4
|
-
buildDisplay,
|
|
5
|
-
displayText,
|
|
6
|
-
marksByDisplay,
|
|
7
|
-
nextWorkBlock,
|
|
8
|
-
overlayMarks,
|
|
9
|
-
spanKey,
|
|
10
|
-
startSpan,
|
|
11
|
-
wrapRuns,
|
|
12
|
-
} from "./view";
|
|
13
|
-
import { cutBlock, parseBlocks, restoreBlock, restoreLine, type Annotation } from "@cueloop/schema";
|
|
14
|
-
|
|
15
|
-
const BASE = `# Plan
|
|
16
|
-
|
|
17
|
-
## Context
|
|
18
|
-
|
|
19
|
-
The daemon persists sessions to disk atomically.
|
|
20
|
-
|
|
21
|
-
- first item
|
|
22
|
-
- second item
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
describe("buildDisplay reconciliation", () => {
|
|
26
|
-
test("no working copy: everything is same", () => {
|
|
27
|
-
const d = buildDisplay(BASE);
|
|
28
|
-
expect(d.every((x) => x.type === "same")).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("reworded block pairs as mod (similar)", () => {
|
|
32
|
-
const working = BASE.replace("persists sessions to disk", "writes sessions to disk");
|
|
33
|
-
const d = buildDisplay(BASE, working);
|
|
34
|
-
const mod = d.find((x) => x.type === "mod")!;
|
|
35
|
-
expect(mod.base!.text).toContain("persists");
|
|
36
|
-
expect(mod.work!.text).toContain("writes");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("unrelated cut + insert do NOT merge into a mod", () => {
|
|
40
|
-
const working = BASE.replace("- second item\n", "") + "\nA totally new closing note.\n";
|
|
41
|
-
const d = buildDisplay(BASE, working);
|
|
42
|
-
expect(d.some((x) => x.type === "del" && x.base!.text === "second item")).toBe(true);
|
|
43
|
-
expect(d.some((x) => x.type === "add" && x.work!.text.includes("closing note"))).toBe(true);
|
|
44
|
-
expect(d.some((x) => x.type === "mod")).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("ordered list items get running numbers", () => {
|
|
48
|
-
const d = buildDisplay("1. one\n2. two\n3. three\n");
|
|
49
|
-
expect(d.map((x) => x.oliNum)).toEqual([1, 2, 3]);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("blockRuns + overlayMarks", () => {
|
|
54
|
-
test("mod blocks emit word-diff runs that reconstruct both sides", () => {
|
|
55
|
-
const d = buildDisplay("plain old text here\n", "plain new text here\n")[0]!;
|
|
56
|
-
const runs = blockRuns(d, true);
|
|
57
|
-
const oldSide = runs.filter((r) => r.role !== "ins").map((r) => r.text).join("");
|
|
58
|
-
const newSide = runs.filter((r) => r.role !== "del").map((r) => r.text).join("");
|
|
59
|
-
expect(oldSide).toBe("plain old text here");
|
|
60
|
-
expect(newSide).toBe("plain new text here");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("marks bind to working-text offsets and skip del runs", () => {
|
|
64
|
-
const d = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
65
|
-
const runs = blockRuns(d, true);
|
|
66
|
-
const marked = overlayMarks(runs, [{ start: 4, end: 9, role: "mark-comment" }]); // "words"
|
|
67
|
-
const markedText = marked.filter((r) => r.role === "mark-comment").map((r) => r.text).join("");
|
|
68
|
-
expect(markedText).toBe("words");
|
|
69
|
-
// del run text untouched
|
|
70
|
-
expect(marked.some((r) => r.role === "del" && r.text === "old")).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("marks never override ins emphasis", () => {
|
|
74
|
-
const d = buildDisplay("keep\n", "keep added\n")[0]!;
|
|
75
|
-
const runs = overlayMarks(blockRuns(d, true), [{ start: 0, end: 10, role: "mark-comment" }]);
|
|
76
|
-
expect(runs.some((r) => r.role === "ins")).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe("marksByDisplay", () => {
|
|
81
|
-
test("annotations resolve into display coordinates", () => {
|
|
82
|
-
const working = BASE.replace("first item", "first item edited beyond recognition of it");
|
|
83
|
-
const display = buildDisplay(BASE, working);
|
|
84
|
-
const ann: Annotation = {
|
|
85
|
-
id: "a1",
|
|
86
|
-
kind: "comment",
|
|
87
|
-
anchor: { quote: "atomically", prefix: "to disk ", suffix: "." },
|
|
88
|
-
body: "define atomically",
|
|
89
|
-
createdAt: "",
|
|
90
|
-
};
|
|
91
|
-
const map = marksByDisplay([ann], display);
|
|
92
|
-
const [dispIdx, marks] = [...map.entries()][0]!;
|
|
93
|
-
expect(displayText(display[dispIdx]!)).toContain("atomically");
|
|
94
|
-
expect(marks[0]!.annotationId).toBe("a1");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("orphaned annotations produce no marks", () => {
|
|
98
|
-
const display = buildDisplay(BASE);
|
|
99
|
-
const ann: Annotation = {
|
|
100
|
-
id: "a1",
|
|
101
|
-
kind: "comment",
|
|
102
|
-
anchor: { quote: "text that is gone", prefix: "", suffix: "" },
|
|
103
|
-
body: "",
|
|
104
|
-
createdAt: "",
|
|
105
|
-
};
|
|
106
|
-
expect(marksByDisplay([ann], display).size).toBe(0);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("wrapRuns", () => {
|
|
111
|
-
test("wraps at word boundaries within width", () => {
|
|
112
|
-
const lines = wrapRuns([{ text: "alpha beta gamma delta", role: "plain", start: 0 }], 11);
|
|
113
|
-
const rendered = lines.map((l) => l.map((r) => r.text).join(""));
|
|
114
|
-
expect(rendered.every((l) => l.length <= 11)).toBe(true);
|
|
115
|
-
expect(rendered.join(" ").replace(/\s+/g, " ").trim()).toBe("alpha beta gamma delta");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("hard-splits words longer than the width", () => {
|
|
119
|
-
const lines = wrapRuns([{ text: "abcdefghij", role: "plain", start: 0 }], 4);
|
|
120
|
-
expect(lines.length).toBeGreaterThan(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("newlines force line breaks (code blocks)", () => {
|
|
124
|
-
const lines = wrapRuns([{ text: "line1\nline2", role: "plain", start: 0 }], 80);
|
|
125
|
-
expect(lines.length).toBe(2);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("span mode", () => {
|
|
130
|
-
const text = "one two three four";
|
|
131
|
-
test("v selects the first word; l grows; w slides; $ to end", () => {
|
|
132
|
-
let s = startSpan(0, text)!;
|
|
133
|
-
expect(text.slice(s.start, s.end)).toBe("one");
|
|
134
|
-
s = spanKey(s, "l", text);
|
|
135
|
-
expect(text.slice(s.start, s.end)).toBe("one two");
|
|
136
|
-
s = spanKey(s, "w", text);
|
|
137
|
-
expect(text.slice(s.start, s.end)).toBe("two three");
|
|
138
|
-
s = spanKey(s, "$", text);
|
|
139
|
-
expect(text.slice(s.start, s.end)).toBe("two three four");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("cut / restore round-trip", () => {
|
|
144
|
-
test("cut removes the block lines; restore puts them back", () => {
|
|
145
|
-
const blocks = parseBlocks(BASE);
|
|
146
|
-
const li = blocks.find((b) => b.text === "second item")!;
|
|
147
|
-
const cut = cutBlock(BASE, li);
|
|
148
|
-
expect(cut).not.toContain("second item");
|
|
149
|
-
const display = buildDisplay(BASE, cut);
|
|
150
|
-
const delIdx = display.findIndex((d) => d.type === "del");
|
|
151
|
-
const line = restoreLine(nextWorkBlock(display, delIdx), cut.split("\n").length);
|
|
152
|
-
// undefined = the working copy round-tripped back to the submitted revision
|
|
153
|
-
const restored = restoreBlock(BASE, cut, li, line);
|
|
154
|
-
expect(restored).toBeUndefined();
|
|
155
|
-
expect(buildDisplay(BASE, restored).every((d) => d.type === "same")).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
});
|