@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.104
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/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.readonly.test.tsx +63 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +94 -12
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +3 -2
- package/src/app/app-view-model.ts +11 -1
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/index.ts +1 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +46 -1
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +79 -1
- package/src/settings/config.ts +91 -1
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +115 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.test.tsx +41 -0
- package/src/thread/components/AgentConfigControls.tsx +8 -1
- package/src/thread/components/AgentThreadPane.test.tsx +91 -3
- package/src/thread/components/AgentThreadPane.tsx +61 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.share.test.ts +47 -2
- package/src/thread/thread-controller.ts +129 -4
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
package/src/diff/split-diff.ts
CHANGED
|
@@ -84,6 +84,7 @@ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
|
|
|
84
84
|
|
|
85
85
|
const flushChangeBlock = (): void => {
|
|
86
86
|
if (pendingDeletions.length === 0 && pendingAdditions.length === 0) return;
|
|
87
|
+
|
|
87
88
|
split.push(...zipChangeBlock(pendingDeletions, pendingAdditions, blockFile));
|
|
88
89
|
pendingDeletions = [];
|
|
89
90
|
pendingAdditions = [];
|
|
@@ -96,14 +97,17 @@ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
|
|
|
96
97
|
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
100
|
+
|
|
99
101
|
if (row.kind === "del") {
|
|
100
102
|
blockFile = row.file;
|
|
101
103
|
pendingDeletions.push({ row, index });
|
|
102
104
|
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
107
|
+
|
|
105
108
|
// any non-change row closes the current change block before it renders
|
|
106
109
|
flushChangeBlock();
|
|
110
|
+
|
|
107
111
|
if (row.kind === "file" || row.kind === "hunk") {
|
|
108
112
|
split.push({ kind: row.kind, file: row.file, text: row.text, rowIndex: index });
|
|
109
113
|
} else
|
|
@@ -135,7 +139,9 @@ export function splitRowOffsets(splitRows: SplitRow[]): Map<number, number> {
|
|
|
135
139
|
contentY += 1;
|
|
136
140
|
} else {
|
|
137
141
|
if (row.left) offsets.set(row.left.rowIndex, contentY);
|
|
142
|
+
|
|
138
143
|
if (row.right) offsets.set(row.right.rowIndex, contentY);
|
|
144
|
+
|
|
139
145
|
contentY += 1;
|
|
140
146
|
}
|
|
141
147
|
}
|
package/src/diff/view-diff.ts
CHANGED
|
@@ -92,10 +92,12 @@ export function fileChangeCounts(
|
|
|
92
92
|
|
|
93
93
|
for (const row of rows) {
|
|
94
94
|
if (row.kind !== "add" && row.kind !== "del") continue;
|
|
95
|
+
|
|
95
96
|
const entry = counts.get(row.file) ?? { additions: 0, deletions: 0 };
|
|
96
97
|
|
|
97
98
|
if (row.kind === "add") entry.additions += 1;
|
|
98
99
|
else entry.deletions += 1;
|
|
100
|
+
|
|
99
101
|
counts.set(row.file, entry);
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -143,9 +145,11 @@ export function marksByRows(
|
|
|
143
145
|
for (const annotation of annotations) {
|
|
144
146
|
// an addressed annotation keeps its record but paints no highlight
|
|
145
147
|
if (isAddressed(annotation)) continue;
|
|
148
|
+
|
|
146
149
|
const resolved = resolveAnchor(annotation.anchor, blocks);
|
|
147
150
|
|
|
148
151
|
if (!resolved) continue;
|
|
152
|
+
|
|
149
153
|
const span: TextSpan = {
|
|
150
154
|
start: { blockIndex: resolved.blockIndex, char: resolved.start },
|
|
151
155
|
end: { blockIndex: resolved.endBlockIndex, char: resolved.end },
|
|
@@ -156,6 +160,7 @@ export function marksByRows(
|
|
|
156
160
|
const range = spanRangeInBlock(span, rowIndex, blocks[rowIndex]!.text.length);
|
|
157
161
|
|
|
158
162
|
if (!range) continue;
|
|
163
|
+
|
|
159
164
|
const marks = marksByIndex.get(rowIndex) ?? [];
|
|
160
165
|
|
|
161
166
|
marks.push({
|
|
@@ -205,6 +210,7 @@ function addResolvedMark(
|
|
|
205
210
|
blocks[rowIndex]!.text.length === 0;
|
|
206
211
|
|
|
207
212
|
if (!range && !marksEmptyLine) continue;
|
|
213
|
+
|
|
208
214
|
const marks = marksByIndex.get(rowIndex) ?? [];
|
|
209
215
|
|
|
210
216
|
marks.push({
|
|
@@ -228,12 +234,14 @@ function addReviewFindingMark(
|
|
|
228
234
|
const finding = annotation.reviewComment;
|
|
229
235
|
|
|
230
236
|
if (!finding || isAddressed(annotation)) return false;
|
|
237
|
+
|
|
231
238
|
const end = resolveReviewAnchorRow(rows, annotation.anchor, finding.path, finding.side);
|
|
232
239
|
const start = finding.startAnchor
|
|
233
240
|
? resolveReviewAnchorRow(rows, finding.startAnchor, finding.path, finding.side)
|
|
234
241
|
: end;
|
|
235
242
|
|
|
236
243
|
if (!start || !end || start.rowIndex > end.rowIndex) return false;
|
|
244
|
+
|
|
237
245
|
addResolvedMark(marksByIndex, annotation, blocks, start.rowIndex, end.rowIndex, focusedId);
|
|
238
246
|
|
|
239
247
|
return true;
|
|
@@ -261,6 +269,7 @@ export function fileRowRange(rows: DiffRow[], path: string): { start: number; en
|
|
|
261
269
|
const start = rows.findIndex((row) => row.file === path);
|
|
262
270
|
|
|
263
271
|
if (start === -1) return null;
|
|
272
|
+
|
|
264
273
|
let end = start;
|
|
265
274
|
|
|
266
275
|
while (end < rows.length && rows[end]!.file === path) end++;
|
|
@@ -284,6 +293,7 @@ export function fileTargetMarks(
|
|
|
284
293
|
const target = annotationTarget(annotation);
|
|
285
294
|
|
|
286
295
|
if (target.kind !== "file") continue;
|
|
296
|
+
|
|
287
297
|
const forPath = byFile.get(target.path) ?? [];
|
|
288
298
|
|
|
289
299
|
forPath.push(annotation);
|
|
@@ -295,6 +305,7 @@ export function fileTargetMarks(
|
|
|
295
305
|
const range = fileRowRange(rows, path);
|
|
296
306
|
|
|
297
307
|
if (!range) continue;
|
|
308
|
+
|
|
298
309
|
const base = range.start;
|
|
299
310
|
const fileRows = rows.slice(range.start, range.end);
|
|
300
311
|
const reviewRootIds = new Set(
|
|
@@ -319,10 +330,12 @@ export function fileTargetMarks(
|
|
|
319
330
|
const finding = annotation.reviewComment;
|
|
320
331
|
|
|
321
332
|
if (!finding || isAddressed(annotation)) continue;
|
|
333
|
+
|
|
322
334
|
if (addReviewFindingMark(fileMarks, annotation, fileRows, blocks, focusedId)) {
|
|
323
335
|
resolvedIds.add(annotation.id);
|
|
324
336
|
continue;
|
|
325
337
|
}
|
|
338
|
+
|
|
326
339
|
const relativeRow = fileRows.findIndex(
|
|
327
340
|
(row) =>
|
|
328
341
|
(finding.side === "RIGHT" ? row.newLine : row.oldLine) === finding.line &&
|
|
@@ -349,11 +362,13 @@ export function fileTargetMarks(
|
|
|
349
362
|
for (const annotation of fileAnnotations) {
|
|
350
363
|
if (!annotation.replyTo || resolvedIds.has(annotation.id) || isAddressed(annotation))
|
|
351
364
|
continue;
|
|
365
|
+
|
|
352
366
|
const parent = [...fileMarks.entries()].find(([, marks]) =>
|
|
353
367
|
marks.some((mark) => mark.annotationId === annotation.replyTo),
|
|
354
368
|
);
|
|
355
369
|
|
|
356
370
|
if (!parent) continue;
|
|
371
|
+
|
|
357
372
|
const [fallbackRow, parentMarks] = parent;
|
|
358
373
|
const parentMark = parentMarks.find((mark) => mark.annotationId === annotation.replyTo)!;
|
|
359
374
|
const replyMark: Mark = {
|
|
@@ -448,6 +463,7 @@ export function commentCountsByFile(session: Thread, rows: DiffRow[]): Map<strin
|
|
|
448
463
|
const file = rows[discussion.blockIndex]?.file;
|
|
449
464
|
|
|
450
465
|
if (file === undefined) continue;
|
|
466
|
+
|
|
451
467
|
counts.set(file, (counts.get(file) ?? 0) + discussion.annotations.length);
|
|
452
468
|
}
|
|
453
469
|
|
|
@@ -496,6 +512,7 @@ export function resolveReviewAnchorRow(
|
|
|
496
512
|
(side === "RIGHT" ? row.newLine === undefined : row.oldLine === undefined)
|
|
497
513
|
)
|
|
498
514
|
return [];
|
|
515
|
+
|
|
499
516
|
const context = diffRowAnchorContext(rows, rowIndex);
|
|
500
517
|
|
|
501
518
|
return [
|
|
@@ -508,6 +525,7 @@ export function resolveReviewAnchorRow(
|
|
|
508
525
|
});
|
|
509
526
|
|
|
510
527
|
if (candidates.length === 0) return undefined;
|
|
528
|
+
|
|
511
529
|
const bestScore = Math.max(...candidates.map((candidate) => candidate.score));
|
|
512
530
|
const best = candidates.filter((candidate) => candidate.score === bestScore);
|
|
513
531
|
|
package/src/diff/walk.ts
CHANGED
|
@@ -32,9 +32,12 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
|
32
32
|
byPath.set(row.file, file);
|
|
33
33
|
files.push(file);
|
|
34
34
|
}
|
|
35
|
+
|
|
35
36
|
if (row.kind !== "add" && row.kind !== "del") continue;
|
|
37
|
+
|
|
36
38
|
if (row.kind === "add") file.added += 1;
|
|
37
39
|
else file.removed += 1;
|
|
40
|
+
|
|
38
41
|
if (file.preview.length < WALK_PREVIEW_ROWS) {
|
|
39
42
|
file.preview.push({
|
|
40
43
|
sign: row.kind === "add" ? "+" : "-",
|
package/src/index.ts
CHANGED
|
@@ -33,9 +33,11 @@ export function resolveDisplayName(
|
|
|
33
33
|
const override = overrides[authorId];
|
|
34
34
|
|
|
35
35
|
if (override) return override;
|
|
36
|
+
|
|
36
37
|
const identity = participants?.find((participant) => participant.id === authorId);
|
|
37
38
|
|
|
38
39
|
if (identity?.name) return identity.name;
|
|
40
|
+
|
|
39
41
|
if (identity) return ANONYMOUS_LABEL;
|
|
40
42
|
|
|
41
43
|
return shortHandle(authorId);
|
|
@@ -65,12 +65,15 @@ export class ClientExtensionRegistry {
|
|
|
65
65
|
): ExtensionUIDisposable {
|
|
66
66
|
if (!/^[a-z][a-z0-9-]*$/.test(contribution.id))
|
|
67
67
|
throw new Error(`Client extension invalid contribution ID: ${contribution.id}`);
|
|
68
|
+
|
|
68
69
|
if (contribution.zone !== zone)
|
|
69
70
|
throw new Error(`Client extension ${owner}:${contribution.id} must use zone ${zone}`);
|
|
71
|
+
|
|
70
72
|
const key = `${owner}:${contribution.id}`;
|
|
71
73
|
|
|
72
74
|
if (this.sections.has(key) || this.actions.has(key) || this.views.has(key))
|
|
73
75
|
throw new Error(`Client extension duplicate contribution: ${key}`);
|
|
76
|
+
|
|
74
77
|
map.set(key, contribution);
|
|
75
78
|
this.changed();
|
|
76
79
|
|
|
@@ -149,6 +152,7 @@ export async function loadInstalledClientExtensions(
|
|
|
149
152
|
const parsed = v.safeParse(v.object({ default: v.function() }), module);
|
|
150
153
|
|
|
151
154
|
if (!parsed.success) throw new Error("client entry point must export a default factory");
|
|
155
|
+
|
|
152
156
|
await registry.load(extension.name, async (api) => {
|
|
153
157
|
await parsed.output.default(api);
|
|
154
158
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
/** The clipboard-write commands to try, most-preferred first, for this platform. */
|
|
9
9
|
export function clipboardCommands(platform: NodeJS.Platform = process.platform): string[][] {
|
|
10
10
|
if (platform === "darwin") return [["pbcopy"]];
|
|
11
|
+
|
|
11
12
|
if (platform === "win32") return [["clip.exe"]];
|
|
12
13
|
|
|
13
14
|
return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
|
|
@@ -36,10 +36,12 @@ for (const enabled of [false, true])
|
|
|
36
36
|
expect(setup.captureCharFrame().includes("Allow agent messages")).toBe(enabled);
|
|
37
37
|
await press(setup, "down");
|
|
38
38
|
await press(setup, "down");
|
|
39
|
+
|
|
39
40
|
if (enabled) {
|
|
40
41
|
await press(setup, " ");
|
|
41
42
|
await press(setup, "down");
|
|
42
43
|
}
|
|
44
|
+
|
|
43
45
|
await press(setup, "enter");
|
|
44
46
|
expect(created).toHaveLength(1);
|
|
45
47
|
expect(created[0]?.agentEnabled).toBe(enabled ? true : undefined);
|
|
@@ -293,7 +293,9 @@ function Wizard({
|
|
|
293
293
|
|
|
294
294
|
useEffect(() => {
|
|
295
295
|
if (focus !== "name") return;
|
|
296
|
+
|
|
296
297
|
nameRef.current?.focus();
|
|
298
|
+
|
|
297
299
|
if (nameRef.current) nameRef.current.cursorOffset = draft.name.length;
|
|
298
300
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
299
301
|
}, [focus]);
|
|
@@ -418,6 +420,7 @@ export function ShareDialog({
|
|
|
418
420
|
|
|
419
421
|
const saveWizard = (): void => {
|
|
420
422
|
if (!wizard || saveDisabled) return;
|
|
423
|
+
|
|
421
424
|
const input: NewShareLink = {
|
|
422
425
|
name: wizard.name.trim() || undefined,
|
|
423
426
|
requireAuth: wizard.requireAuth,
|
|
@@ -425,13 +428,16 @@ export function ShareDialog({
|
|
|
425
428
|
};
|
|
426
429
|
|
|
427
430
|
if (agentAvailable) input.agentEnabled = wizard.agentEnabled ?? false;
|
|
431
|
+
|
|
428
432
|
if (wizard.editingId) onUpdateLink(wizard.editingId, input);
|
|
429
433
|
else onCreateLink(input);
|
|
434
|
+
|
|
430
435
|
backToList();
|
|
431
436
|
};
|
|
432
437
|
|
|
433
438
|
const activateListRow = (): void => {
|
|
434
439
|
if (listCursor >= links.length) return openWizardWith(newWizardDraft(threadName));
|
|
440
|
+
|
|
435
441
|
const link = links[listCursor];
|
|
436
442
|
|
|
437
443
|
if (link) onCopyLink(link.id);
|
|
@@ -440,30 +446,41 @@ export function ShareDialog({
|
|
|
440
446
|
// turning auth on drops the cursor straight into the handles field; turning it off resets them
|
|
441
447
|
const toggleAuth = (): void => {
|
|
442
448
|
if (!wizard) return;
|
|
449
|
+
|
|
443
450
|
const next = !wizard.requireAuth;
|
|
444
451
|
|
|
445
452
|
shareDialogStore.getState().setRequireAuth(next);
|
|
453
|
+
|
|
446
454
|
if (!next) shareDialogStore.getState().setAllowlist([]);
|
|
455
|
+
|
|
447
456
|
setWizardFocus(next ? "allowlist" : "auth");
|
|
448
457
|
};
|
|
449
458
|
|
|
450
459
|
// grammar keys only bite outside a focused text field, where they cannot be typed
|
|
451
460
|
const handleWizardKey = (name: string): void => {
|
|
452
461
|
if (!wizard) return;
|
|
462
|
+
|
|
453
463
|
if (name === "escape") return backToList();
|
|
464
|
+
|
|
454
465
|
if (name === "down") return moveWizardFocus(1);
|
|
466
|
+
|
|
455
467
|
if (name === "up") return moveWizardFocus(-1);
|
|
468
|
+
|
|
456
469
|
const activated = name === "return" || name === "enter";
|
|
457
470
|
|
|
458
471
|
if (wizardFocus === "auth" && (activated || name === "space")) return toggleAuth();
|
|
472
|
+
|
|
459
473
|
if (wizardFocus === "agent" && (activated || name === "space"))
|
|
460
474
|
return shareDialogStore.getState().setAgentEnabled(!wizard.agentEnabled);
|
|
475
|
+
|
|
461
476
|
if (wizardFocus === "actions" && activated) saveWizard();
|
|
462
477
|
};
|
|
463
478
|
|
|
464
479
|
const handleNavKey = (name: string): void => {
|
|
465
480
|
if (name === "j" || name === "down") return shareDialogStore.getState().setCategory("export");
|
|
481
|
+
|
|
466
482
|
if (name === "k" || name === "up") return shareDialogStore.getState().setCategory("external");
|
|
483
|
+
|
|
467
484
|
const enter =
|
|
468
485
|
name === "l" || name === "right" || name === "return" || name === "enter" || name === "tab";
|
|
469
486
|
|
|
@@ -478,21 +495,32 @@ export function ShareDialog({
|
|
|
478
495
|
|
|
479
496
|
const handleBodyKey = (name: string): void => {
|
|
480
497
|
if (name === "h" || name === "left") return setActiveZone("nav");
|
|
498
|
+
|
|
481
499
|
if (name === "j" || name === "down")
|
|
482
500
|
return setListCursor((cursor) => Math.min(cursor + 1, rowCount - 1));
|
|
501
|
+
|
|
483
502
|
if (name === "k" || name === "up") return setListCursor((cursor) => Math.max(cursor - 1, 0));
|
|
503
|
+
|
|
484
504
|
if (name === "return" || name === "enter") return activateListRow();
|
|
505
|
+
|
|
485
506
|
if (listCursor >= links.length) return;
|
|
507
|
+
|
|
486
508
|
if (name === "e") return editLinkAtCursor();
|
|
509
|
+
|
|
487
510
|
if (name === "d") onDeleteLink(links[listCursor]!.id);
|
|
488
511
|
};
|
|
489
512
|
|
|
490
513
|
useKeyboard((key) => {
|
|
491
514
|
if (!isOpen) return;
|
|
515
|
+
|
|
492
516
|
if (wizard) return handleWizardKey(key.name);
|
|
517
|
+
|
|
493
518
|
if (menuOpenId !== null) return void (key.name === "escape" && setMenuOpenId(null));
|
|
519
|
+
|
|
494
520
|
if (key.name === "escape") return onClose();
|
|
521
|
+
|
|
495
522
|
if (activeZone === "nav") return handleNavKey(key.name);
|
|
523
|
+
|
|
496
524
|
handleBodyKey(key.name);
|
|
497
525
|
});
|
|
498
526
|
|
|
@@ -85,6 +85,7 @@ export function resolveEditorCommand(rawEditor: string): ResolvedEditor {
|
|
|
85
85
|
const base = editorBaseName(parts[0] ?? "");
|
|
86
86
|
|
|
87
87
|
if (TERMINAL_EDITORS.has(base)) return { argv: parts, waits: true };
|
|
88
|
+
|
|
88
89
|
const waitFlags = GUI_WAIT_FLAGS[base];
|
|
89
90
|
|
|
90
91
|
if (waitFlags) {
|
|
@@ -103,6 +104,7 @@ const NO_WAIT_THRESHOLD_MS = 1000;
|
|
|
103
104
|
|
|
104
105
|
function suspectsNoWait(resolved: ResolvedEditor, elapsedMs: number, unchanged: boolean): boolean {
|
|
105
106
|
if (resolved.waits) return false; // holds the terminal or has a wait flag - trust the exit
|
|
107
|
+
|
|
106
108
|
if (!unchanged) return false; // edits landed, so it waited after all
|
|
107
109
|
|
|
108
110
|
return elapsedMs < NO_WAIT_THRESHOLD_MS;
|
|
@@ -145,6 +147,7 @@ export function editInEditor(
|
|
|
145
147
|
});
|
|
146
148
|
|
|
147
149
|
if (editorProcess.exitCode !== 0) throw new Error(`editor exited ${editorProcess.exitCode}`);
|
|
150
|
+
|
|
148
151
|
let next = readFileSync(path, "utf8");
|
|
149
152
|
|
|
150
153
|
if (suspectsNoWait(resolved, now() - startedAt, next === content)) {
|
|
@@ -10,6 +10,7 @@ const tempDirectories: string[] = [];
|
|
|
10
10
|
afterEach(() => {
|
|
11
11
|
if (originalBinary === undefined) delete process.env.CUELOOP_GH;
|
|
12
12
|
else process.env.CUELOOP_GH = originalBinary;
|
|
13
|
+
|
|
13
14
|
for (const directory of tempDirectories.splice(0)) {
|
|
14
15
|
rmSync(directory, { recursive: true, force: true });
|
|
15
16
|
}
|
|
@@ -21,9 +21,11 @@ export function focusHerdrPane(binPath: string, paneId: string): boolean {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
if (got.exitCode !== 0) return false;
|
|
24
|
+
|
|
24
25
|
const parsed = v.safeParse(HerdrPaneSchema, JSON.parse(got.stdout.toString()));
|
|
25
26
|
|
|
26
27
|
if (!parsed.success) return false;
|
|
28
|
+
|
|
27
29
|
const tab = parsed.output.result?.pane?.tab_id;
|
|
28
30
|
|
|
29
31
|
if (!tab) return false;
|
|
@@ -37,6 +37,7 @@ function isControlCodePoint(codePoint: number): boolean {
|
|
|
37
37
|
/** True when a paste is an image or other non-text binary payload and should collapse to a placeholder. */
|
|
38
38
|
export function looksLikeBinaryPaste(bytes: Uint8Array): boolean {
|
|
39
39
|
if (bytes.length === 0) return false;
|
|
40
|
+
|
|
40
41
|
if (hasImageMagic(bytes)) return true;
|
|
41
42
|
|
|
42
43
|
const decoded = PASTE_DECODER.decode(bytes);
|
|
@@ -48,6 +49,7 @@ export function looksLikeBinaryPaste(bytes: Uint8Array): boolean {
|
|
|
48
49
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
49
50
|
|
|
50
51
|
if (codePoint === 0 || codePoint === 0xfffd) return true;
|
|
52
|
+
|
|
51
53
|
if (isControlCodePoint(codePoint)) controlCount += 1;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -19,9 +19,10 @@ import {
|
|
|
19
19
|
type SharePolicy,
|
|
20
20
|
type ShareLink,
|
|
21
21
|
type Thread,
|
|
22
|
+
type ThreadAgentState,
|
|
22
23
|
} from "@cueloop/schema";
|
|
23
24
|
import { ThreadRecordSchema } from "@cueloop/daemon/validate";
|
|
24
|
-
import type { SharedMerge } from "@cueloop/daemon/client";
|
|
25
|
+
import type { ThreadClient, SharedMerge } from "@cueloop/daemon/client";
|
|
25
26
|
import * as v from "valibot";
|
|
26
27
|
import { copyToClipboard } from "./clipboard";
|
|
27
28
|
|
|
@@ -62,9 +63,11 @@ export async function publishShare(
|
|
|
62
63
|
|
|
63
64
|
if (code !== 0)
|
|
64
65
|
throw new Error(`gateway upload failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
66
|
+
|
|
65
67
|
const line = stdout.trim();
|
|
66
68
|
|
|
67
69
|
if (!line.startsWith("ssh ")) throw new Error(`unexpected gateway reply: ${line || "(empty)"}`);
|
|
70
|
+
|
|
68
71
|
const copied = await copyToClipboard(line);
|
|
69
72
|
|
|
70
73
|
return { line, copied };
|
|
@@ -98,6 +101,7 @@ export function mergeFromShare(remote: Thread): SharedMerge {
|
|
|
98
101
|
if (remote.shares?.length === 1) merge.shareId = remote.shares[0]!.id;
|
|
99
102
|
|
|
100
103
|
if (remote.participants) merge.participants = remote.participants;
|
|
104
|
+
|
|
101
105
|
if (remote.history) {
|
|
102
106
|
merge.removals = removalEntries(remote.history).map((entry) => ({
|
|
103
107
|
id: entry.id,
|
|
@@ -193,6 +197,7 @@ async function readLines(
|
|
|
193
197
|
pending = lines.pop() ?? "";
|
|
194
198
|
for (const line of lines) {
|
|
195
199
|
if (line.trim() === "") continue;
|
|
200
|
+
|
|
196
201
|
try {
|
|
197
202
|
onLine(line);
|
|
198
203
|
} catch {
|
|
@@ -236,3 +241,43 @@ async function runShareSsh(
|
|
|
236
241
|
|
|
237
242
|
return { stdout, stderr, code };
|
|
238
243
|
}
|
|
244
|
+
|
|
245
|
+
/** Freeze the visible conversation without owner session IDs, execution data or delivery receipts. */
|
|
246
|
+
export async function snapshotThreadConversation(
|
|
247
|
+
thread: Thread,
|
|
248
|
+
client: Pick<ThreadClient, "agentGet">,
|
|
249
|
+
): Promise<Thread> {
|
|
250
|
+
if (!client.agentGet) return thread;
|
|
251
|
+
|
|
252
|
+
let state: ThreadAgentState;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
state = await client.agentGet(thread.id);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof Error && error.message.startsWith("Thread agent is disabled;"))
|
|
258
|
+
return thread;
|
|
259
|
+
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!state.messages.length && !state.submissions?.length) return thread;
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...thread,
|
|
267
|
+
agentSnapshot: {
|
|
268
|
+
threadId: thread.id,
|
|
269
|
+
harness: state.harness ? { id: state.harness.id, label: state.harness.label } : undefined,
|
|
270
|
+
phase: { kind: "idle" },
|
|
271
|
+
messages: state.messages,
|
|
272
|
+
comments: state.comments,
|
|
273
|
+
tools: [],
|
|
274
|
+
submissions: state.submissions?.map(({ id, commentId, prompt, quote, status }) => ({
|
|
275
|
+
id,
|
|
276
|
+
commentId,
|
|
277
|
+
prompt,
|
|
278
|
+
quote,
|
|
279
|
+
status,
|
|
280
|
+
})),
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -77,6 +77,7 @@ export function HandleChipsInput({
|
|
|
77
77
|
const input = inputRef.current;
|
|
78
78
|
|
|
79
79
|
if (!input || !focused) return;
|
|
80
|
+
|
|
80
81
|
input.focus();
|
|
81
82
|
input.cursorOffset = seed.length; // land the caret at the end of the seeded text
|
|
82
83
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -91,12 +92,15 @@ export function HandleChipsInput({
|
|
|
91
92
|
const handle = (inputRef.current?.plainText ?? "").trim().replace(/^@/, "").toLowerCase();
|
|
92
93
|
|
|
93
94
|
if (handle && !logins.includes(handle)) onChange([...logins, handle]);
|
|
95
|
+
|
|
94
96
|
reseed("");
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
useKeyboard((key) => {
|
|
98
100
|
if (!focused) return;
|
|
101
|
+
|
|
99
102
|
if (key.name !== "backspace" || (inputRef.current?.plainText ?? "").length > 0) return;
|
|
103
|
+
|
|
100
104
|
// backspace on an empty field pulls the last chip back in to edit; with none left, collapse
|
|
101
105
|
if (logins.length > 0) {
|
|
102
106
|
const last = logins[logins.length - 1]!;
|
|
@@ -46,10 +46,13 @@ export function SlashComposer({
|
|
|
46
46
|
|
|
47
47
|
useKeyboard((key) => {
|
|
48
48
|
if (!slashActive) return;
|
|
49
|
+
|
|
49
50
|
const selected = Math.min(slashIndex, items.length - 1);
|
|
50
51
|
|
|
51
52
|
if (key.name === "up") return setSlashIndex(Math.max(0, selected - 1));
|
|
53
|
+
|
|
52
54
|
if (key.name === "down") return setSlashIndex(Math.min(items.length - 1, selected + 1));
|
|
55
|
+
|
|
53
56
|
if (key.name === "tab" || (key.name === "return" && !key.meta && !key.ctrl && !key.super)) {
|
|
54
57
|
const inserted = insertSlashItem(text, caret, items[selected]!.name);
|
|
55
58
|
|
|
@@ -405,11 +405,13 @@ export class KeyBindings {
|
|
|
405
405
|
fragments.push(entry.text);
|
|
406
406
|
continue;
|
|
407
407
|
}
|
|
408
|
+
|
|
408
409
|
const displays = entry.commands
|
|
409
410
|
.map((command) => this.keyDisplayFor(command))
|
|
410
411
|
.filter((display): display is string => display !== null);
|
|
411
412
|
|
|
412
413
|
if (!displays.length) continue;
|
|
414
|
+
|
|
413
415
|
const keyPart = displays.join("/");
|
|
414
416
|
|
|
415
417
|
if (!entry.label) fragments.push(keyPart);
|
|
@@ -435,6 +437,7 @@ export class KeyBindings {
|
|
|
435
437
|
|
|
436
438
|
for (const entry of HINT_TEMPLATES[mode]) {
|
|
437
439
|
if ("text" in entry || !entry.label) continue;
|
|
440
|
+
|
|
438
441
|
const keys = entry.commands
|
|
439
442
|
.map((command) => this.keyDisplayFor(command))
|
|
440
443
|
.filter((display): display is string => display !== null)
|