@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +148 -0
- package/src/use-annotation-surface.tsx +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- 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 -312
package/src/config.test.ts
CHANGED
|
@@ -2,57 +2,317 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_KEYS,
|
|
7
|
+
DEFAULT_QUICK_ACTIONS,
|
|
8
|
+
actionFor,
|
|
9
|
+
loadConfig,
|
|
10
|
+
persistAuthorName,
|
|
11
|
+
persistActions,
|
|
12
|
+
persistPins,
|
|
13
|
+
persistTheme,
|
|
14
|
+
quickActionBody,
|
|
15
|
+
resolveQuickAction,
|
|
16
|
+
} from "./config";
|
|
6
17
|
import { DARK } from "./theme";
|
|
18
|
+
import { themeForName } from "./theme-presets";
|
|
7
19
|
|
|
8
20
|
describe("loadConfig", () => {
|
|
9
21
|
test("defaults when no file exists", () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
// Act
|
|
23
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(config.keys["comment"]).toEqual(["c"]);
|
|
27
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
13
28
|
});
|
|
14
29
|
|
|
15
30
|
test("user config rebinds actions and overrides theme tokens", () => {
|
|
31
|
+
// Arrange
|
|
16
32
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
|
|
17
33
|
const path = join(dir, "config.toml");
|
|
34
|
+
|
|
18
35
|
writeFileSync(
|
|
19
36
|
path,
|
|
20
37
|
`[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
|
|
21
38
|
);
|
|
39
|
+
|
|
22
40
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
expect(
|
|
41
|
+
// Act
|
|
42
|
+
const config = loadConfig({ userConfigPath: path });
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(config.keys["comment"]).toEqual(["a"]);
|
|
46
|
+
expect(config.keys["submit"]).toEqual(["return", "S"]);
|
|
47
|
+
expect(config.keys["cut"]).toEqual(["x"]); // untouched defaults survive
|
|
48
|
+
expect(config.theme.accent).toBe("#ff0000");
|
|
49
|
+
expect(config.theme.green).toBe(DARK.green);
|
|
29
50
|
} finally {
|
|
30
51
|
rmSync(dir, { recursive: true, force: true });
|
|
31
52
|
}
|
|
32
53
|
});
|
|
33
54
|
|
|
34
55
|
test("repo config layers over user config", () => {
|
|
56
|
+
// Arrange
|
|
35
57
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
|
|
36
58
|
const user = join(dir, "user.toml");
|
|
37
59
|
const repoRoot = join(dir, "repo");
|
|
60
|
+
|
|
38
61
|
writeFileSync(user, `[keys]\ncomment = "a"\n`);
|
|
39
62
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
40
63
|
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
|
|
64
|
+
|
|
41
65
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
// Act
|
|
67
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
expect(config.keys["comment"]).toEqual(["z"]);
|
|
44
71
|
} finally {
|
|
45
72
|
rmSync(dir, { recursive: true, force: true });
|
|
46
73
|
}
|
|
47
74
|
});
|
|
48
75
|
|
|
49
76
|
test("broken config never blocks a review", () => {
|
|
77
|
+
// Arrange
|
|
50
78
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
|
|
51
79
|
const path = join(dir, "config.toml");
|
|
80
|
+
|
|
52
81
|
writeFileSync(path, "not [valid toml");
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Act
|
|
85
|
+
const config = loadConfig({ userConfigPath: path });
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
expect(config.keys["comment"]).toEqual(["c"]);
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("[ui] parses auto_close and the editor override", () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg4-"));
|
|
97
|
+
const path = join(dir, "config.toml");
|
|
98
|
+
|
|
99
|
+
writeFileSync(path, `[ui]\nauto_close = 3\neditor = "code --wait"\n`);
|
|
100
|
+
|
|
53
101
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
// Act
|
|
103
|
+
const config = loadConfig({ userConfigPath: path });
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
expect(config.ui.autoClose).toBe(3);
|
|
107
|
+
expect(config.ui.editor).toBe("code --wait");
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("[ui] editor defaults to undefined and ignores a blank value", () => {
|
|
114
|
+
// Arrange
|
|
115
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg5-"));
|
|
116
|
+
const path = join(dir, "config.toml");
|
|
117
|
+
|
|
118
|
+
writeFileSync(path, `[ui]\neditor = " "\n`);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Assert
|
|
122
|
+
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.editor).toBeUndefined();
|
|
123
|
+
expect(loadConfig({ userConfigPath: path }).ui.editor).toBeUndefined();
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("[ui] diff_view defaults to split and parses stacked", () => {
|
|
130
|
+
// Arrange
|
|
131
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
|
|
132
|
+
const path = join(dir, "config.toml");
|
|
133
|
+
|
|
134
|
+
writeFileSync(path, `[ui]\ndiff_view = "stacked"\n`);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Assert
|
|
138
|
+
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.diffView).toBe("split");
|
|
139
|
+
expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
|
|
140
|
+
} finally {
|
|
141
|
+
rmSync(dir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("[skills] path overrides the default and a malformed value never discards the config", () => {
|
|
146
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-skills-"));
|
|
147
|
+
const path = join(dir, "config.toml");
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(path, `[skills]\npath = "/custom/skills"\n`);
|
|
151
|
+
expect(loadConfig({ userConfigPath: path }).skillsPath).toBe("/custom/skills");
|
|
152
|
+
|
|
153
|
+
// a wrong-typed path falls back to the default without dropping the sibling [keys] setting
|
|
154
|
+
writeFileSync(path, `[skills]\npath = 123\n\n[keys]\ncomment = "z"\n`);
|
|
155
|
+
const config = loadConfig({ userConfigPath: path });
|
|
156
|
+
|
|
157
|
+
expect(config.keys["comment"]).toEqual(["z"]);
|
|
158
|
+
expect(config.skillsPath).toContain(".agents");
|
|
159
|
+
} finally {
|
|
160
|
+
rmSync(dir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('[ui] diff_view loads the pre-rename "unified" value as "stacked"', () => {
|
|
165
|
+
// Arrange - an upgrade must not flip a user who had persisted the old spelling
|
|
166
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
|
|
167
|
+
const path = join(dir, "config.toml");
|
|
168
|
+
|
|
169
|
+
writeFileSync(path, `[ui]\ndiff_view = "unified"\n`);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Assert
|
|
173
|
+
expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
|
|
174
|
+
} finally {
|
|
175
|
+
rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("[ui] theme defaults to the branded cueloop preset", () => {
|
|
180
|
+
// Act
|
|
181
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
182
|
+
|
|
183
|
+
// Assert
|
|
184
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
185
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("[ui] theme selects a named preset as the token base", () => {
|
|
189
|
+
// Arrange
|
|
190
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-"));
|
|
191
|
+
const path = join(dir, "config.toml");
|
|
192
|
+
|
|
193
|
+
writeFileSync(path, `[ui]\ntheme = "nord"\n`);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// Act
|
|
197
|
+
const config = loadConfig({ userConfigPath: path });
|
|
198
|
+
|
|
199
|
+
// Assert
|
|
200
|
+
expect(config.ui.theme).toBe("nord");
|
|
201
|
+
expect(config.theme).toEqual(themeForName("nord"));
|
|
202
|
+
} finally {
|
|
203
|
+
rmSync(dir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("[theme] token overrides layer on top of the selected preset", () => {
|
|
208
|
+
// Arrange
|
|
209
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme2-"));
|
|
210
|
+
const path = join(dir, "config.toml");
|
|
211
|
+
|
|
212
|
+
writeFileSync(path, `[ui]\ntheme = "nord"\n\n[theme]\naccent = "#ff0000"\n`);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Act
|
|
216
|
+
const config = loadConfig({ userConfigPath: path });
|
|
217
|
+
|
|
218
|
+
// Assert
|
|
219
|
+
expect(config.theme.accent).toBe("#ff0000"); // override wins
|
|
220
|
+
expect(config.theme.background).toBe(themeForName("nord").background); // preset base survives
|
|
221
|
+
expect(config.themeOverrides).toEqual({ accent: "#ff0000" }); // deltas exposed for live re-compose
|
|
222
|
+
} finally {
|
|
223
|
+
rmSync(dir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("a later file's preset keeps an earlier file's [theme] override", () => {
|
|
228
|
+
// Arrange - user overrides accent; repo picks a preset in a separate file
|
|
229
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-x-"));
|
|
230
|
+
const user = join(dir, "user.toml");
|
|
231
|
+
const repoRoot = join(dir, "repo");
|
|
232
|
+
|
|
233
|
+
writeFileSync(user, `[theme]\naccent = "#ff0000"\n`);
|
|
234
|
+
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
235
|
+
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[ui]\ntheme = "nord"\n`);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Act
|
|
239
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
240
|
+
|
|
241
|
+
// Assert - the preset is the base, but the earlier override still wins its token
|
|
242
|
+
expect(config.ui.theme).toBe("nord");
|
|
243
|
+
expect(config.theme.background).toBe(themeForName("nord").background);
|
|
244
|
+
expect(config.theme.accent).toBe("#ff0000");
|
|
245
|
+
} finally {
|
|
246
|
+
rmSync(dir, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("[ui] ignores an unknown theme name and keeps the default", () => {
|
|
251
|
+
// Arrange
|
|
252
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme3-"));
|
|
253
|
+
const path = join(dir, "config.toml");
|
|
254
|
+
|
|
255
|
+
writeFileSync(path, `[ui]\ntheme = "solarized-galaxy"\n`);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Act
|
|
259
|
+
const config = loadConfig({ userConfigPath: path });
|
|
260
|
+
|
|
261
|
+
// Assert
|
|
262
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
263
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
264
|
+
} finally {
|
|
265
|
+
rmSync(dir, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("persistTheme round-trips through the config file", () => {
|
|
270
|
+
// Arrange
|
|
271
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme4-"));
|
|
272
|
+
const path = join(dir, "config.toml");
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
// Act
|
|
276
|
+
persistTheme("tokyo-night", path);
|
|
277
|
+
|
|
278
|
+
// Assert
|
|
279
|
+
expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("tokyo-night");
|
|
280
|
+
|
|
281
|
+
// a second write replaces the key in place
|
|
282
|
+
// Act
|
|
283
|
+
persistTheme("gruvbox-dark", path);
|
|
284
|
+
|
|
285
|
+
// Assert
|
|
286
|
+
expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("gruvbox-dark");
|
|
287
|
+
} finally {
|
|
288
|
+
rmSync(dir, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("persistPins round-trips the pinned-thread ids through the config file", () => {
|
|
293
|
+
// Arrange
|
|
294
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-pins-"));
|
|
295
|
+
const path = join(dir, "config.toml");
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Act
|
|
299
|
+
persistPins(["s_one", "s_two"], path);
|
|
300
|
+
|
|
301
|
+
// Assert
|
|
302
|
+
expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_one", "s_two"]);
|
|
303
|
+
|
|
304
|
+
// unpinning rewrites the list in place, including down to empty
|
|
305
|
+
// Act
|
|
306
|
+
persistPins(["s_two"], path);
|
|
307
|
+
|
|
308
|
+
// Assert
|
|
309
|
+
expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_two"]);
|
|
310
|
+
|
|
311
|
+
// Act
|
|
312
|
+
persistPins([], path);
|
|
313
|
+
|
|
314
|
+
// Assert
|
|
315
|
+
expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual([]);
|
|
56
316
|
} finally {
|
|
57
317
|
rmSync(dir, { recursive: true, force: true });
|
|
58
318
|
}
|
|
@@ -61,56 +321,226 @@ describe("loadConfig", () => {
|
|
|
61
321
|
|
|
62
322
|
describe("integrations.obsidian config", () => {
|
|
63
323
|
test("defaults when the section is absent", () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
324
|
+
// Act
|
|
325
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
326
|
+
|
|
327
|
+
// Assert
|
|
328
|
+
expect(config.integrations.obsidian.vault).toBeUndefined();
|
|
329
|
+
expect(config.integrations.obsidian.folder).toBe("cueloop");
|
|
330
|
+
expect(config.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
|
|
331
|
+
expect(config.integrations.obsidian.separator).toBe("space");
|
|
332
|
+
expect(config.integrations.obsidian.exportOn).toBe("manual");
|
|
70
333
|
});
|
|
71
334
|
|
|
72
335
|
test("user config sets the section; invalid enum values are ignored", () => {
|
|
336
|
+
// Arrange
|
|
73
337
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
|
|
74
338
|
const path = join(dir, "config.toml");
|
|
339
|
+
|
|
75
340
|
writeFileSync(
|
|
76
341
|
path,
|
|
77
342
|
`[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
|
|
78
343
|
);
|
|
344
|
+
|
|
79
345
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
346
|
+
// Act
|
|
347
|
+
const config = loadConfig({ userConfigPath: path });
|
|
348
|
+
|
|
349
|
+
// Assert
|
|
350
|
+
expect(config.integrations.obsidian.vault).toBe("/notes/vault");
|
|
351
|
+
expect(config.integrations.obsidian.folder).toBe("plans");
|
|
352
|
+
expect(config.integrations.obsidian.exportOn).toBe("approve");
|
|
353
|
+
expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
|
|
354
|
+
expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
|
|
86
355
|
} finally {
|
|
87
356
|
rmSync(dir, { recursive: true, force: true });
|
|
88
357
|
}
|
|
89
358
|
});
|
|
90
359
|
|
|
91
360
|
test("repo config layers over user config for the section", () => {
|
|
361
|
+
// Arrange
|
|
92
362
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
|
|
93
363
|
const user = join(dir, "user.toml");
|
|
94
364
|
const repoRoot = join(dir, "repo");
|
|
365
|
+
|
|
95
366
|
writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
|
|
96
367
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
97
|
-
writeFileSync(
|
|
368
|
+
writeFileSync(
|
|
369
|
+
join(repoRoot, ".cueloop", "config.toml"),
|
|
370
|
+
`[integrations.obsidian]\nfolder = "repo-plans"\n`,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Act
|
|
375
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
376
|
+
|
|
377
|
+
// Assert
|
|
378
|
+
expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
|
|
379
|
+
expect(config.integrations.obsidian.exportOn).toBe("resolve");
|
|
380
|
+
expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
|
|
381
|
+
} finally {
|
|
382
|
+
rmSync(dir, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("quick actions ([[actions]])", () => {
|
|
388
|
+
test("absent config keeps the built-in defaults", () => {
|
|
389
|
+
// Act
|
|
390
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
391
|
+
|
|
392
|
+
// Assert
|
|
393
|
+
expect(config.actions).toEqual(DEFAULT_QUICK_ACTIONS);
|
|
394
|
+
expect(config.actions).toHaveLength(8);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("configured actions replace the defaults, with optional metadata", () => {
|
|
398
|
+
// Arrange
|
|
399
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions-"));
|
|
400
|
+
const path = join(dir, "config.toml");
|
|
401
|
+
|
|
402
|
+
writeFileSync(
|
|
403
|
+
path,
|
|
404
|
+
`[[actions]]\nprompt = "Add a benchmark"\n\n[[actions]]\nprompt = "Guard the edge case"\nmetadata = "null and empty input"\n`,
|
|
405
|
+
);
|
|
406
|
+
|
|
98
407
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
408
|
+
// Act
|
|
409
|
+
const config = loadConfig({ userConfigPath: path });
|
|
410
|
+
|
|
411
|
+
// Assert
|
|
412
|
+
expect(config.actions).toEqual([
|
|
413
|
+
{ prompt: "Add a benchmark" },
|
|
414
|
+
{ prompt: "Guard the edge case", metadata: "null and empty input" },
|
|
415
|
+
]);
|
|
416
|
+
} finally {
|
|
417
|
+
rmSync(dir, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("a table missing prompt is skipped; the rest replace the defaults", () => {
|
|
422
|
+
// Arrange
|
|
423
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions2-"));
|
|
424
|
+
const path = join(dir, "config.toml");
|
|
425
|
+
|
|
426
|
+
writeFileSync(
|
|
427
|
+
path,
|
|
428
|
+
`[[actions]]\nmetadata = "orphan without a prompt"\n\n[[actions]]\nprompt = "Keep this one"\n`,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Act
|
|
433
|
+
const config = loadConfig({ userConfigPath: path });
|
|
434
|
+
|
|
435
|
+
// Assert
|
|
436
|
+
expect(config.actions).toEqual([{ prompt: "Keep this one" }]);
|
|
437
|
+
} finally {
|
|
438
|
+
rmSync(dir, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("persistActions", () => {
|
|
444
|
+
test("round-trips the action set through [[actions]] and replaces prior ones", () => {
|
|
445
|
+
// Arrange
|
|
446
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-persist-actions-"));
|
|
447
|
+
const path = join(dir, "config.toml");
|
|
448
|
+
|
|
449
|
+
writeFileSync(path, `[[actions]]\nprompt = "old one"\n\n[ui]\nauto_close = 3\n`);
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Act
|
|
453
|
+
persistActions(
|
|
454
|
+
[{ prompt: "Zoom out" }, { prompt: "Prototype", metadata: "skip tests and polish" }],
|
|
455
|
+
path,
|
|
456
|
+
);
|
|
457
|
+
const config = loadConfig({ userConfigPath: path });
|
|
458
|
+
|
|
459
|
+
// Assert - the old action is gone, the new set is read back, and [ui] survives
|
|
460
|
+
expect(config.actions).toEqual([
|
|
461
|
+
{ prompt: "Zoom out" },
|
|
462
|
+
{ prompt: "Prototype", metadata: "skip tests and polish" },
|
|
463
|
+
]);
|
|
464
|
+
expect(config.ui.autoClose).toBe(3);
|
|
103
465
|
} finally {
|
|
104
466
|
rmSync(dir, { recursive: true, force: true });
|
|
105
467
|
}
|
|
106
468
|
});
|
|
107
469
|
});
|
|
108
470
|
|
|
471
|
+
describe("quickActionBody", () => {
|
|
472
|
+
test("appends the system prompt below the prompt when set", () => {
|
|
473
|
+
// Assert
|
|
474
|
+
expect(quickActionBody({ prompt: "Prototype this", metadata: "skip tests" })).toBe(
|
|
475
|
+
"Prototype this\n\nskip tests",
|
|
476
|
+
);
|
|
477
|
+
expect(quickActionBody({ prompt: "Out of scope" })).toBe("Out of scope");
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("resolveQuickAction", () => {
|
|
482
|
+
test("resolves by 1-based index and by case-insensitive prompt", () => {
|
|
483
|
+
// Assert
|
|
484
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "1")).toEqual(DEFAULT_QUICK_ACTIONS[0]!);
|
|
485
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "out of scope")).toEqual(
|
|
486
|
+
DEFAULT_QUICK_ACTIONS[2]!,
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("returns undefined for an out-of-range index or an unknown name", () => {
|
|
491
|
+
// Assert
|
|
492
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "0")).toBeUndefined();
|
|
493
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "99")).toBeUndefined();
|
|
494
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "no such action")).toBeUndefined();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
109
498
|
describe("actionFor", () => {
|
|
110
499
|
test("resolves keys and shifted keys to actions", () => {
|
|
500
|
+
// Assert
|
|
111
501
|
expect(actionFor(DEFAULT_KEYS, "j", false)).toBe("down");
|
|
112
502
|
expect(actionFor(DEFAULT_KEYS, "g", true)).toBe("bottom"); // G
|
|
113
503
|
expect(actionFor(DEFAULT_KEYS, "return", false)).toBe("submit");
|
|
114
504
|
expect(actionFor(DEFAULT_KEYS, "zz", false)).toBeUndefined();
|
|
115
505
|
});
|
|
116
506
|
});
|
|
507
|
+
|
|
508
|
+
describe("[authors] rename map", () => {
|
|
509
|
+
test("loads quoted fingerprint keys", () => {
|
|
510
|
+
// Arrange
|
|
511
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-authors-"));
|
|
512
|
+
const path = join(dir, "config.toml");
|
|
513
|
+
|
|
514
|
+
writeFileSync(path, `[authors]\n"SHA256:abc+def/gh" = "Alex"\n`);
|
|
515
|
+
|
|
516
|
+
// Act
|
|
517
|
+
const config = loadConfig({ userConfigPath: path });
|
|
518
|
+
|
|
519
|
+
// Assert
|
|
520
|
+
expect(config.authors["SHA256:abc+def/gh"]).toBe("Alex");
|
|
521
|
+
rmSync(dir, { recursive: true, force: true });
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("persistAuthorName round-trips and updates a fingerprint id in place", () => {
|
|
525
|
+
// Arrange
|
|
526
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-authors2-"));
|
|
527
|
+
const path = join(dir, "config.toml");
|
|
528
|
+
|
|
529
|
+
// Act / Assert
|
|
530
|
+
persistAuthorName("SHA256:abc+def/gh", "Alex", path);
|
|
531
|
+
expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alex");
|
|
532
|
+
persistAuthorName("SHA256:abc+def/gh", "Alexa", path);
|
|
533
|
+
expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alexa");
|
|
534
|
+
rmSync(dir, { recursive: true, force: true });
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe("the /lgtm default quick action", () => {
|
|
539
|
+
test("expands to a terse approving note", () => {
|
|
540
|
+
const lgtm = DEFAULT_QUICK_ACTIONS.find((action) => action.prompt.toLowerCase() === "lgtm");
|
|
541
|
+
|
|
542
|
+
expect(lgtm).toBeDefined();
|
|
543
|
+
// the reference is /lgtm, so the label is the prompt; it expands like every action (label, then note)
|
|
544
|
+
expect(quickActionBody(lgtm!)).toBe("LGTM\n\nThis looks good to me.");
|
|
545
|
+
});
|
|
546
|
+
});
|