@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
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/package.json +5 -5
- package/src/App.autoclose.test.tsx +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
|
|
22
22
|
/** One shared VT library load; null when this platform ships no prebuilt dylib. */
|
|
23
23
|
let ghosttyFactory: GhosttyTerminalFactory | null | undefined;
|
|
24
|
+
|
|
24
25
|
function factory(): GhosttyTerminalFactory | null {
|
|
25
26
|
if (ghosttyFactory === undefined) ghosttyFactory = loadGhosttyTerminals();
|
|
27
|
+
|
|
26
28
|
return ghosttyFactory;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -71,8 +73,10 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
71
73
|
/** Start the child + emulator once real layout dimensions are known. */
|
|
72
74
|
private start(cols: number, rows: number): void {
|
|
73
75
|
const terminals = factory();
|
|
76
|
+
|
|
74
77
|
if (!terminals) return;
|
|
75
78
|
const vt = terminals.create(cols, rows);
|
|
79
|
+
|
|
76
80
|
if (!vt) return; // no emulator, no visible screen - do not spawn a blind child
|
|
77
81
|
this.vt = vt;
|
|
78
82
|
this.cols = cols;
|
|
@@ -107,6 +111,7 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
107
111
|
if (width <= 0 || height <= 0) return;
|
|
108
112
|
if (!this.pty) {
|
|
109
113
|
this.start(width, height);
|
|
114
|
+
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
if (width === this.cols && height === this.rows) return;
|
|
@@ -121,11 +126,13 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
121
126
|
for (let y = 0; y < this.height; y++) {
|
|
122
127
|
for (let x = 0; x < this.width; x++) {
|
|
123
128
|
const cell = this.vt.readCell(x, y);
|
|
129
|
+
|
|
124
130
|
// Skip an empty normal-width cell; the buffer already starts blank.
|
|
125
131
|
if (cell && cell.width === 0 && cell.codepoint === 0) continue;
|
|
126
132
|
const char = cell && cell.codepoint ? String.fromCodePoint(cell.codepoint) : " ";
|
|
127
133
|
let fg = cell ? resolveColor(cell.fg, DEFAULT_FG) : DEFAULT_FG;
|
|
128
134
|
let bg = cell ? resolveColor(cell.bg, TRANSPARENT) : TRANSPARENT;
|
|
135
|
+
|
|
129
136
|
if (cell?.inverse) [fg, bg] = [bg, fg];
|
|
130
137
|
const attributes = cell
|
|
131
138
|
? createTextAttributes({
|
|
@@ -136,15 +143,18 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
136
143
|
strikethrough: cell.strikethrough,
|
|
137
144
|
})
|
|
138
145
|
: 0;
|
|
146
|
+
|
|
139
147
|
buffer.setCell(this.x + x, this.y + y, char, fg, bg, attributes);
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
// cursor is active-area coords; equal to viewport at the bottom (the live
|
|
143
151
|
// typing case). It can drift if the child scrolls the viewport back.
|
|
144
152
|
const cursor = this.vt.readCursor();
|
|
153
|
+
|
|
145
154
|
if (cursor.visible && cursor.x < this.width && cursor.y < this.height) {
|
|
146
155
|
const under = this.vt.readCell(cursor.x, cursor.y);
|
|
147
156
|
const char = under && under.codepoint ? String.fromCodePoint(under.codepoint) : " ";
|
|
157
|
+
|
|
148
158
|
// a block cursor: paint the cell with fg/bg swapped
|
|
149
159
|
buffer.setCell(this.x + cursor.x, this.y + cursor.y, char, CURSOR_INK, DEFAULT_FG, 0);
|
|
150
160
|
}
|
|
@@ -169,10 +179,12 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
169
179
|
function resolveColor(color: GhosttyColor, fallback: RGBA): RGBA {
|
|
170
180
|
if (color.kind === "rgb") return RGBA.fromInts(color.r, color.g, color.b, 255);
|
|
171
181
|
if (color.kind === "palette") return RGBA.fromIndex(color.index);
|
|
182
|
+
|
|
172
183
|
return fallback;
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
let registered = false;
|
|
187
|
+
|
|
176
188
|
/** Register `<terminalPane>` with the OpenTUI React reconciler (idempotent). */
|
|
177
189
|
export function registerTerminalPane(): void {
|
|
178
190
|
if (registered) return;
|
package/src/config.test.ts
CHANGED
|
@@ -33,6 +33,7 @@ describe("loadConfig", () => {
|
|
|
33
33
|
// Arrange
|
|
34
34
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
|
|
35
35
|
const path = join(dir, "config.toml");
|
|
36
|
+
|
|
36
37
|
writeFileSync(
|
|
37
38
|
path,
|
|
38
39
|
`[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
|
|
@@ -58,6 +59,7 @@ describe("loadConfig", () => {
|
|
|
58
59
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
|
|
59
60
|
const user = join(dir, "user.toml");
|
|
60
61
|
const repoRoot = join(dir, "repo");
|
|
62
|
+
|
|
61
63
|
writeFileSync(user, `[keys]\ncomment = "a"\n`);
|
|
62
64
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
63
65
|
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
|
|
@@ -77,6 +79,7 @@ describe("loadConfig", () => {
|
|
|
77
79
|
// Arrange
|
|
78
80
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
|
|
79
81
|
const path = join(dir, "config.toml");
|
|
82
|
+
|
|
80
83
|
writeFileSync(path, "not [valid toml");
|
|
81
84
|
|
|
82
85
|
try {
|
|
@@ -94,6 +97,7 @@ describe("loadConfig", () => {
|
|
|
94
97
|
// Arrange
|
|
95
98
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg4-"));
|
|
96
99
|
const path = join(dir, "config.toml");
|
|
100
|
+
|
|
97
101
|
writeFileSync(path, `[ui]\nauto_close = 3\neditor = "code --wait"\n`);
|
|
98
102
|
|
|
99
103
|
try {
|
|
@@ -112,6 +116,7 @@ describe("loadConfig", () => {
|
|
|
112
116
|
// Arrange
|
|
113
117
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg5-"));
|
|
114
118
|
const path = join(dir, "config.toml");
|
|
119
|
+
|
|
115
120
|
writeFileSync(path, `[ui]\neditor = " "\n`);
|
|
116
121
|
|
|
117
122
|
try {
|
|
@@ -136,6 +141,7 @@ describe("loadConfig", () => {
|
|
|
136
141
|
// Arrange
|
|
137
142
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg6-"));
|
|
138
143
|
const path = join(dir, "config.toml");
|
|
144
|
+
|
|
139
145
|
writeFileSync(path, `[ui]\nreview_width = 999\nreview_state = "compact"\n`);
|
|
140
146
|
|
|
141
147
|
try {
|
|
@@ -154,6 +160,7 @@ describe("loadConfig", () => {
|
|
|
154
160
|
// Arrange
|
|
155
161
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg7-"));
|
|
156
162
|
const path = join(dir, "config.toml");
|
|
163
|
+
|
|
157
164
|
writeFileSync(path, `[ui]\nreview_width = "wide"\nreview_state = "sideways"\n`);
|
|
158
165
|
|
|
159
166
|
try {
|
|
@@ -181,6 +188,7 @@ describe("loadConfig", () => {
|
|
|
181
188
|
// Arrange
|
|
182
189
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-"));
|
|
183
190
|
const path = join(dir, "config.toml");
|
|
191
|
+
|
|
184
192
|
writeFileSync(path, `[ui]\ntheme = "nord"\n`);
|
|
185
193
|
|
|
186
194
|
try {
|
|
@@ -199,6 +207,7 @@ describe("loadConfig", () => {
|
|
|
199
207
|
// Arrange
|
|
200
208
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme2-"));
|
|
201
209
|
const path = join(dir, "config.toml");
|
|
210
|
+
|
|
202
211
|
writeFileSync(path, `[ui]\ntheme = "nord"\n\n[theme]\naccent = "#ff0000"\n`);
|
|
203
212
|
|
|
204
213
|
try {
|
|
@@ -219,6 +228,7 @@ describe("loadConfig", () => {
|
|
|
219
228
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-x-"));
|
|
220
229
|
const user = join(dir, "user.toml");
|
|
221
230
|
const repoRoot = join(dir, "repo");
|
|
231
|
+
|
|
222
232
|
writeFileSync(user, `[theme]\naccent = "#ff0000"\n`);
|
|
223
233
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
224
234
|
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[ui]\ntheme = "nord"\n`);
|
|
@@ -240,6 +250,7 @@ describe("loadConfig", () => {
|
|
|
240
250
|
// Arrange
|
|
241
251
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme3-"));
|
|
242
252
|
const path = join(dir, "config.toml");
|
|
253
|
+
|
|
243
254
|
writeFileSync(path, `[ui]\ntheme = "solarized-galaxy"\n`);
|
|
244
255
|
|
|
245
256
|
try {
|
|
@@ -289,6 +300,7 @@ describe("loadConfig", () => {
|
|
|
289
300
|
|
|
290
301
|
// Assert
|
|
291
302
|
const config = loadConfig({ userConfigPath: path });
|
|
303
|
+
|
|
292
304
|
expect(config.ui.reviewWidth).toBe(42);
|
|
293
305
|
expect(config.ui.reviewState).toBe("hidden");
|
|
294
306
|
|
|
@@ -321,6 +333,7 @@ describe("integrations.obsidian config", () => {
|
|
|
321
333
|
// Arrange
|
|
322
334
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
|
|
323
335
|
const path = join(dir, "config.toml");
|
|
336
|
+
|
|
324
337
|
writeFileSync(
|
|
325
338
|
path,
|
|
326
339
|
`[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
|
|
@@ -346,6 +359,7 @@ describe("integrations.obsidian config", () => {
|
|
|
346
359
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
|
|
347
360
|
const user = join(dir, "user.toml");
|
|
348
361
|
const repoRoot = join(dir, "repo");
|
|
362
|
+
|
|
349
363
|
writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
|
|
350
364
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
351
365
|
writeFileSync(
|
|
@@ -381,6 +395,7 @@ describe("quick actions ([[actions]])", () => {
|
|
|
381
395
|
// Arrange
|
|
382
396
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions-"));
|
|
383
397
|
const path = join(dir, "config.toml");
|
|
398
|
+
|
|
384
399
|
writeFileSync(
|
|
385
400
|
path,
|
|
386
401
|
`[[actions]]\nprompt = "Add a benchmark"\n\n[[actions]]\nprompt = "Guard the edge case"\nmetadata = "null and empty input"\n`,
|
|
@@ -404,6 +419,7 @@ describe("quick actions ([[actions]])", () => {
|
|
|
404
419
|
// Arrange
|
|
405
420
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions2-"));
|
|
406
421
|
const path = join(dir, "config.toml");
|
|
422
|
+
|
|
407
423
|
writeFileSync(
|
|
408
424
|
path,
|
|
409
425
|
`[[actions]]\nmetadata = "orphan without a prompt"\n\n[[actions]]\nprompt = "Keep this one"\n`,
|
|
@@ -426,6 +442,7 @@ describe("persistActions", () => {
|
|
|
426
442
|
// Arrange
|
|
427
443
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-persist-actions-"));
|
|
428
444
|
const path = join(dir, "config.toml");
|
|
445
|
+
|
|
429
446
|
writeFileSync(path, `[[actions]]\nprompt = "old one"\n\n[ui]\nauto_close = 3\n`);
|
|
430
447
|
|
|
431
448
|
try {
|
|
@@ -490,6 +507,7 @@ describe("[authors] rename map", () => {
|
|
|
490
507
|
// Arrange
|
|
491
508
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-authors-"));
|
|
492
509
|
const path = join(dir, "config.toml");
|
|
510
|
+
|
|
493
511
|
writeFileSync(path, `[authors]\n"SHA256:abc+def/gh" = "Alex"\n`);
|
|
494
512
|
|
|
495
513
|
// Act
|
package/src/config.ts
CHANGED
|
@@ -80,8 +80,10 @@ export function resolveQuickAction(
|
|
|
80
80
|
actionRef: string,
|
|
81
81
|
): QuickAction | undefined {
|
|
82
82
|
const index = Number(actionRef);
|
|
83
|
+
|
|
83
84
|
if (Number.isInteger(index) && index >= 1 && index <= actions.length) return actions[index - 1];
|
|
84
85
|
const wanted = actionRef.trim().toLowerCase();
|
|
86
|
+
|
|
85
87
|
return actions.find((action) => action.prompt.toLowerCase() === wanted);
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -147,21 +149,26 @@ function parseToml(text: string): Record<string, unknown> {
|
|
|
147
149
|
function parseActions(raw: unknown): QuickAction[] | undefined {
|
|
148
150
|
if (!Array.isArray(raw)) return undefined;
|
|
149
151
|
const actions: QuickAction[] = [];
|
|
152
|
+
|
|
150
153
|
for (const entry of raw) {
|
|
151
154
|
if (!entry || typeof entry !== "object") continue;
|
|
152
155
|
const table = entry as Record<string, unknown>;
|
|
153
156
|
const prompt = table["prompt"];
|
|
157
|
+
|
|
154
158
|
if (typeof prompt !== "string" || !prompt.trim()) continue;
|
|
155
159
|
const metadata = table["metadata"];
|
|
160
|
+
|
|
156
161
|
actions.push(
|
|
157
162
|
typeof metadata === "string" && metadata.trim() ? { prompt, metadata } : { prompt },
|
|
158
163
|
);
|
|
159
164
|
}
|
|
165
|
+
|
|
160
166
|
return actions.length ? actions : undefined;
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
function mergeAuthors(target: Record<string, string>, raw: unknown): void {
|
|
164
170
|
const authors = raw as Record<string, unknown> | undefined;
|
|
171
|
+
|
|
165
172
|
if (!authors) return;
|
|
166
173
|
for (const [id, value] of Object.entries(authors)) {
|
|
167
174
|
if (typeof value === "string") target[id] = value;
|
|
@@ -170,6 +177,7 @@ function mergeAuthors(target: Record<string, string>, raw: unknown): void {
|
|
|
170
177
|
|
|
171
178
|
function mergeKeys(target: Record<string, string[]>, raw: unknown): void {
|
|
172
179
|
const keys = raw as KeymapConfig | undefined;
|
|
180
|
+
|
|
173
181
|
if (!keys) return;
|
|
174
182
|
for (const [action, combo] of Object.entries(keys)) {
|
|
175
183
|
target[action] = Array.isArray(combo) ? combo : [combo];
|
|
@@ -186,6 +194,7 @@ function mergeUi(target: CueloopConfig["ui"], raw: unknown): void {
|
|
|
186
194
|
const ui = raw as
|
|
187
195
|
| { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
|
|
188
196
|
| undefined;
|
|
197
|
+
|
|
189
198
|
if (!ui) return;
|
|
190
199
|
mergeAutoClose(target, ui.auto_close);
|
|
191
200
|
if (typeof ui.editor === "string" && ui.editor.trim()) target.editor = ui.editor.trim();
|
|
@@ -204,15 +213,18 @@ function mergeUi(target: CueloopConfig["ui"], raw: unknown): void {
|
|
|
204
213
|
function mergeObsidian(target: ObsidianConfig, raw: unknown): void {
|
|
205
214
|
const integrations = raw as Record<string, unknown> | undefined;
|
|
206
215
|
const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
|
|
216
|
+
|
|
207
217
|
if (!obsidian) return;
|
|
208
218
|
if (typeof obsidian["vault"] === "string") target.vault = obsidian["vault"];
|
|
209
219
|
if (typeof obsidian["folder"] === "string") target.folder = obsidian["folder"];
|
|
210
220
|
if (typeof obsidian["filenameFormat"] === "string")
|
|
211
221
|
target.filenameFormat = obsidian["filenameFormat"];
|
|
212
222
|
const separator = obsidian["separator"];
|
|
223
|
+
|
|
213
224
|
if (separator === "space" || separator === "dash" || separator === "underscore")
|
|
214
225
|
target.separator = separator;
|
|
215
226
|
const exportOn = obsidian["exportOn"];
|
|
227
|
+
|
|
216
228
|
if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
|
|
217
229
|
target.exportOn = exportOn;
|
|
218
230
|
}
|
|
@@ -228,11 +240,13 @@ function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig
|
|
|
228
240
|
integrations: { obsidian: { ...base.integrations.obsidian } },
|
|
229
241
|
};
|
|
230
242
|
const actions = parseActions(raw["actions"]);
|
|
243
|
+
|
|
231
244
|
if (actions) out.actions = actions;
|
|
232
245
|
mergeAuthors(out.authors, raw["authors"]);
|
|
233
246
|
mergeKeys(out.keys, raw["keys"]);
|
|
234
247
|
mergeUi(out.ui, raw["ui"]);
|
|
235
248
|
mergeObsidian(out.integrations.obsidian, raw["integrations"]);
|
|
249
|
+
|
|
236
250
|
return out;
|
|
237
251
|
}
|
|
238
252
|
|
|
@@ -260,6 +274,7 @@ export function loadConfig(
|
|
|
260
274
|
let themeName = DEFAULT_THEME_NAME;
|
|
261
275
|
const themeOverrides: Partial<Record<keyof Theme, string>> = {};
|
|
262
276
|
const userPath = userConfigPathFrom(options.userConfigPath);
|
|
277
|
+
|
|
263
278
|
for (const path of [
|
|
264
279
|
userPath,
|
|
265
280
|
options.repoRoot ? join(options.repoRoot, ".cueloop", "config.toml") : undefined,
|
|
@@ -267,8 +282,10 @@ export function loadConfig(
|
|
|
267
282
|
if (!path || !existsSync(path)) continue;
|
|
268
283
|
try {
|
|
269
284
|
const raw = parseToml(readFileSync(path, "utf8"));
|
|
285
|
+
|
|
270
286
|
config = layer(config, raw);
|
|
271
287
|
const rawTheme = (raw["ui"] as { theme?: unknown } | undefined)?.theme;
|
|
288
|
+
|
|
272
289
|
if (typeof rawTheme === "string" && isThemeName(rawTheme)) themeName = rawTheme;
|
|
273
290
|
collectThemeOverrides(raw["theme"], themeOverrides);
|
|
274
291
|
} catch {
|
|
@@ -278,6 +295,7 @@ export function loadConfig(
|
|
|
278
295
|
config.ui.theme = themeName;
|
|
279
296
|
config.themeOverrides = themeOverrides;
|
|
280
297
|
config.theme = { ...themeForName(themeName), ...themeOverrides };
|
|
298
|
+
|
|
281
299
|
return config;
|
|
282
300
|
}
|
|
283
301
|
|
|
@@ -296,9 +314,11 @@ export function actionFor(
|
|
|
296
314
|
shift: boolean,
|
|
297
315
|
): string | undefined {
|
|
298
316
|
const wanted = shift && name.length === 1 ? name.toUpperCase() : name;
|
|
317
|
+
|
|
299
318
|
for (const [action, combos] of Object.entries(keys)) {
|
|
300
319
|
if (combos.includes(wanted)) return action;
|
|
301
320
|
}
|
|
321
|
+
|
|
302
322
|
return undefined;
|
|
303
323
|
}
|
|
304
324
|
|
|
@@ -319,8 +339,10 @@ function userConfigPathFrom(userConfigPath?: string): string {
|
|
|
319
339
|
function persistUiSetting(key: string, rendered: string, userConfigPath?: string): void {
|
|
320
340
|
const path = userConfigPathFrom(userConfigPath);
|
|
321
341
|
let text = "";
|
|
342
|
+
|
|
322
343
|
if (existsSync(path)) text = readFileSync(path, "utf8");
|
|
323
344
|
const assignment = new RegExp(`^(\\s*)${key}\\s*=.*$`, "m");
|
|
345
|
+
|
|
324
346
|
if (assignment.test(text)) {
|
|
325
347
|
text = text.replace(assignment, `$1${key} = ${rendered}`);
|
|
326
348
|
} else if (/^\[ui\]/m.test(text)) {
|
|
@@ -345,15 +367,19 @@ function tomlString(value: string): string {
|
|
|
345
367
|
export function persistActions(actions: QuickAction[], userConfigPath?: string): void {
|
|
346
368
|
const path = userConfigPathFrom(userConfigPath);
|
|
347
369
|
let text = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
370
|
+
|
|
348
371
|
// strip every existing [[actions]] block (header through its key lines)
|
|
349
372
|
text = text.replace(/^\[\[actions\]\][^[]*/gm, "").trimEnd();
|
|
350
373
|
const blocks = actions
|
|
351
374
|
.map((action) => {
|
|
352
375
|
const lines = [`[[actions]]`, `prompt = ${tomlString(action.prompt)}`];
|
|
376
|
+
|
|
353
377
|
if (action.metadata) lines.push(`metadata = ${tomlString(action.metadata)}`);
|
|
378
|
+
|
|
354
379
|
return lines.join("\n");
|
|
355
380
|
})
|
|
356
381
|
.join("\n\n");
|
|
382
|
+
|
|
357
383
|
text = blocks ? `${text ? `${text}\n\n` : ""}${blocks}\n` : `${text}\n`;
|
|
358
384
|
mkdirSync(dirname(path), { recursive: true });
|
|
359
385
|
writeFileSync(path, text);
|
|
@@ -393,6 +419,7 @@ export function persistAuthorName(id: string, name: string, userConfigPath?: str
|
|
|
393
419
|
const key = `"${id.replace(/"/g, '\\"')}"`;
|
|
394
420
|
const value = `"${name.replace(/"/g, '\\"')}"`;
|
|
395
421
|
const assignment = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*=.*$`, "m");
|
|
422
|
+
|
|
396
423
|
if (assignment.test(text)) {
|
|
397
424
|
text = text.replace(assignment, `$1${key} = ${value}`);
|
|
398
425
|
} else if (/^\[authors\]/m.test(text)) {
|
package/src/diff-hunk-curate.ts
CHANGED
|
@@ -62,6 +62,7 @@ export function locateLine(
|
|
|
62
62
|
lineNumber: number,
|
|
63
63
|
): LocatedLine | null {
|
|
64
64
|
const index = lineNumber - 1;
|
|
65
|
+
|
|
65
66
|
for (const [hunkIndex, hunk] of model.hunks.entries()) {
|
|
66
67
|
for (const [contentIndex, content] of hunk.hunkContent.entries()) {
|
|
67
68
|
const base = side === "addition" ? content.additionLineIndex : content.deletionLineIndex;
|
|
@@ -71,11 +72,13 @@ export function locateLine(
|
|
|
71
72
|
: side === "addition"
|
|
72
73
|
? content.additions
|
|
73
74
|
: content.deletions;
|
|
75
|
+
|
|
74
76
|
if (count > 0 && index >= base && index < base + count) {
|
|
75
77
|
return { hunkIndex, changeIndex: content.type === "change" ? contentIndex : undefined };
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
|
|
79
82
|
return null;
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -87,6 +90,7 @@ function changeSideLine(
|
|
|
87
90
|
return { side: "addition", lineNumber: row.newLine };
|
|
88
91
|
if (row.kind === "del" && row.oldLine !== undefined)
|
|
89
92
|
return { side: "deletion", lineNumber: row.oldLine };
|
|
93
|
+
|
|
90
94
|
return null;
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -107,6 +111,7 @@ export function hunkRejectionForRow(
|
|
|
107
111
|
: row.oldLine !== undefined
|
|
108
112
|
? locateLine(model, "deletion", row.oldLine)
|
|
109
113
|
: null;
|
|
114
|
+
|
|
110
115
|
return located ? { path, hunkIndex: located.hunkIndex } : null;
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -120,9 +125,12 @@ export function changeRejectionForRow(
|
|
|
120
125
|
row: Pick<DiffRow, "kind" | "oldLine" | "newLine">,
|
|
121
126
|
): HunkRejection | null {
|
|
122
127
|
const change = changeSideLine(row);
|
|
128
|
+
|
|
123
129
|
if (!change) return null;
|
|
124
130
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
131
|
+
|
|
125
132
|
if (!located || located.changeIndex === undefined) return null;
|
|
133
|
+
|
|
126
134
|
return { path, hunkIndex: located.hunkIndex, changeIndex: located.changeIndex };
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -152,9 +160,12 @@ export function isRowRejected(
|
|
|
152
160
|
rejections: HunkRejection[],
|
|
153
161
|
): boolean {
|
|
154
162
|
const change = changeSideLine(row);
|
|
163
|
+
|
|
155
164
|
if (!change) return false;
|
|
156
165
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
166
|
+
|
|
157
167
|
if (!located || located.changeIndex === undefined) return false;
|
|
168
|
+
|
|
158
169
|
return rejections.some(
|
|
159
170
|
(rejection) =>
|
|
160
171
|
rejection.path === path &&
|
|
@@ -178,6 +189,7 @@ function curateFilePatch(file: DiffFileContents, rejections: HunkRejection[]): s
|
|
|
178
189
|
.filter((rejection) => rejection.changeIndex === undefined)
|
|
179
190
|
.map((rejection) => rejection.hunkIndex),
|
|
180
191
|
);
|
|
192
|
+
|
|
181
193
|
for (const hunkIndex of [...wholeHunks].sort((a, b) => b - a)) {
|
|
182
194
|
model = diffAcceptRejectHunk(model, hunkIndex, "reject");
|
|
183
195
|
}
|
|
@@ -186,6 +198,7 @@ function curateFilePatch(file: DiffFileContents, rejections: HunkRejection[]): s
|
|
|
186
198
|
(rejection) => rejection.changeIndex !== undefined && !wholeHunks.has(rejection.hunkIndex),
|
|
187
199
|
)
|
|
188
200
|
.sort((a, b) => b.hunkIndex - a.hunkIndex || b.changeIndex! - a.changeIndex!);
|
|
201
|
+
|
|
189
202
|
for (const rejection of changeRejections) {
|
|
190
203
|
model = diffAcceptRejectHunk(model, rejection.hunkIndex, {
|
|
191
204
|
type: "reject",
|
|
@@ -193,9 +206,12 @@ function curateFilePatch(file: DiffFileContents, rejections: HunkRejection[]): s
|
|
|
193
206
|
});
|
|
194
207
|
}
|
|
195
208
|
const curatedNew = model.additionLines.join("");
|
|
209
|
+
|
|
196
210
|
if (curatedNew === file.oldContents) return null;
|
|
197
211
|
const patch = unifiedDiffText(file.oldContents, curatedNew, file.path);
|
|
212
|
+
|
|
198
213
|
if (patch === null) return null;
|
|
214
|
+
|
|
199
215
|
return withFileStateHeaders(patch, file.path, file.status, curatedNew);
|
|
200
216
|
}
|
|
201
217
|
|
|
@@ -215,6 +231,7 @@ function withFileStateHeaders(
|
|
|
215
231
|
if (status === "deleted" && curatedNew === "") {
|
|
216
232
|
return patch.replace(`+++ b/${path}`, "+++ /dev/null");
|
|
217
233
|
}
|
|
234
|
+
|
|
218
235
|
return patch;
|
|
219
236
|
}
|
|
220
237
|
|
|
@@ -224,10 +241,13 @@ function withFileStateHeaders(
|
|
|
224
241
|
*/
|
|
225
242
|
export function curateDiff(files: DiffFileContents[], rejections: HunkRejection[]): string {
|
|
226
243
|
const parts: string[] = [];
|
|
244
|
+
|
|
227
245
|
for (const file of files) {
|
|
228
246
|
const fileRejections = rejections.filter((rejection) => rejection.path === file.path);
|
|
229
247
|
const patch = curateFilePatch(file, fileRejections);
|
|
248
|
+
|
|
230
249
|
if (patch) parts.push(patch);
|
|
231
250
|
}
|
|
251
|
+
|
|
232
252
|
return parts.join("\n");
|
|
233
253
|
}
|
|
@@ -76,6 +76,7 @@ describe("intralineRunsByRow", () => {
|
|
|
76
76
|
// Assert
|
|
77
77
|
const deletionRuns = runsByRow.get(1)!;
|
|
78
78
|
const additionRuns = runsByRow.get(2)!;
|
|
79
|
+
|
|
79
80
|
expect(joinText(deletionRuns)).toBe("const foo = 1");
|
|
80
81
|
expect(joinText(additionRuns)).toBe("const foo = 2");
|
|
81
82
|
expect(changedText(deletionRuns)).toBe("1");
|
package/src/diff-intraline.ts
CHANGED
|
@@ -47,14 +47,17 @@ function stripTrailingNewline(text: string): string {
|
|
|
47
47
|
export function intralineRunsByRow(rows: DiffRow[]): Map<number, IntralineRun[]> {
|
|
48
48
|
const runsByRow = new Map<number, IntralineRun[]>();
|
|
49
49
|
let index = 0;
|
|
50
|
+
|
|
50
51
|
while (index < rows.length) {
|
|
51
52
|
if (rows[index]!.kind !== "del") {
|
|
52
53
|
index++;
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
55
56
|
const deletionStart = index;
|
|
57
|
+
|
|
56
58
|
while (index < rows.length && rows[index]!.kind === "del") index++;
|
|
57
59
|
const additionStart = index;
|
|
60
|
+
|
|
58
61
|
while (index < rows.length && rows[index]!.kind === "add") index++;
|
|
59
62
|
const deletionTexts = rows
|
|
60
63
|
.slice(deletionStart, additionStart)
|
|
@@ -62,15 +65,18 @@ export function intralineRunsByRow(rows: DiffRow[]): Map<number, IntralineRun[]>
|
|
|
62
65
|
const additionTexts = rows
|
|
63
66
|
.slice(additionStart, index)
|
|
64
67
|
.map((row) => stripTrailingNewline(row.text));
|
|
68
|
+
|
|
65
69
|
for (const [deletionOffset, additionOffset] of alignedPairs(deletionTexts, additionTexts)) {
|
|
66
70
|
const changes = wordLevelChanges(
|
|
67
71
|
deletionTexts[deletionOffset]!,
|
|
68
72
|
additionTexts[additionOffset]!,
|
|
69
73
|
);
|
|
74
|
+
|
|
70
75
|
runsByRow.set(deletionStart + deletionOffset, sideRuns(changes, "removed"));
|
|
71
76
|
runsByRow.set(additionStart + additionOffset, sideRuns(changes, "added"));
|
|
72
77
|
}
|
|
73
78
|
}
|
|
79
|
+
|
|
74
80
|
return runsByRow;
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -83,10 +89,13 @@ function linesSimilar(oldText: string, newText: string): boolean {
|
|
|
83
89
|
const words = (text: string) => new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
|
|
84
90
|
const oldWords = words(oldText);
|
|
85
91
|
const newWords = words(newText);
|
|
92
|
+
|
|
86
93
|
if (oldWords.size === 0 && newWords.size === 0) return true;
|
|
87
94
|
let intersection = 0;
|
|
95
|
+
|
|
88
96
|
for (const word of oldWords) if (newWords.has(word)) intersection++;
|
|
89
97
|
const union = oldWords.size + newWords.size - intersection;
|
|
98
|
+
|
|
90
99
|
return union === 0 ? true : intersection / union >= SIMILAR_MIN_WORD_OVERLAP;
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -103,6 +112,7 @@ function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[
|
|
|
103
112
|
const pairs: Array<[number, number]> = [];
|
|
104
113
|
let deletionOffset = 0;
|
|
105
114
|
let additionOffset = 0;
|
|
115
|
+
|
|
106
116
|
for (const op of lcsDiff(deletionTexts, additionTexts, linesSimilar)) {
|
|
107
117
|
if (op.kind === "ctx") {
|
|
108
118
|
pairs.push([deletionOffset, additionOffset]);
|
|
@@ -114,6 +124,7 @@ function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[
|
|
|
114
124
|
additionOffset++;
|
|
115
125
|
}
|
|
116
126
|
}
|
|
127
|
+
|
|
117
128
|
return pairs;
|
|
118
129
|
}
|
|
119
130
|
|
|
@@ -121,12 +132,15 @@ function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[
|
|
|
121
132
|
* runs of equal changed-ness coalesced so highlighting stays as few spans. */
|
|
122
133
|
function sideRuns(changes: WordChange[], side: "added" | "removed"): IntralineRun[] {
|
|
123
134
|
const runs: IntralineRun[] = [];
|
|
135
|
+
|
|
124
136
|
for (const change of changes) {
|
|
125
137
|
if (change.kind !== "common" && change.kind !== side) continue;
|
|
126
138
|
const changed = change.kind === side;
|
|
127
139
|
const last = runs[runs.length - 1];
|
|
140
|
+
|
|
128
141
|
if (last && last.changed === changed) last.text += change.text;
|
|
129
142
|
else runs.push({ text: change.text, changed });
|
|
130
143
|
}
|
|
144
|
+
|
|
131
145
|
return runs;
|
|
132
146
|
}
|