@cueloop/client 0.1.0-alpha.98 → 0.1.0-alpha.99
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 +11 -11
- package/src/annotations/components/AnnotationCards.tsx +2 -2
- package/src/annotations/use-annotation-surface.tsx +13 -2
- package/src/app/App.deleted-scroll.test.tsx +1 -0
- package/src/app/App.diff.test.tsx +1 -0
- package/src/app/App.identity.test.tsx +1 -0
- package/src/app/App.regression.test.tsx +5 -0
- package/src/app/App.share-dialog.test.tsx +5 -0
- package/src/app/App.submit-confirm.test.tsx +3 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +6 -0
- package/src/app/App.visual.test.tsx +2 -0
- package/src/app/App.workbench.test.tsx +53 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/components/AppShell.tsx +1 -0
- package/src/app/run.ts +1 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
- package/src/diff/components/AnnotatableFileView.tsx +2 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +1 -0
- package/src/diff/diff-intraline.test.ts +1 -0
- package/src/diff/diff-intraline.ts +2 -0
- package/src/diff/split-diff.ts +2 -0
- package/src/integrations/components/ShareDialog.tsx +1 -0
- package/src/markdown/components/ThreadView.tsx +2 -0
- package/src/markdown/mark-runs.ts +1 -0
- package/src/markdown/markdown-highlight.ts +2 -0
- package/src/performance/otlp-trace.test.ts +3 -0
- package/src/performance/otlp-trace.ts +4 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/settings/components/quick-actions-editor.test.tsx +3 -0
- package/src/stories/stories-app.tsx +3 -0
- package/src/stories/story.ts +2 -0
- package/src/terminal/pty.ts +1 -0
- package/src/thread/agent-transcript.ts +4 -0
- package/src/thread/components/AgentConfigControls.test.tsx +2 -0
- package/src/thread/components/AgentThreadPane.test.tsx +54 -5
- package/src/thread/components/AgentThreadPane.tsx +11 -0
- package/src/thread/components/ThreadFooter.test.tsx +2 -0
- package/src/thread/components/thread-tree.test.ts +3 -0
- package/src/thread/components/thread-tree.ts +4 -0
- package/src/thread/thread-controller.curate.test.ts +1 -0
- package/src/thread/thread-controller.live-diff.test.ts +7 -0
- package/src/thread/thread-controller.share.test.ts +1 -0
- package/src/thread/thread-controller.ts +8 -0
- package/src/thread/use-thread-agent.test.tsx +143 -0
- package/src/thread/use-thread-agent.ts +22 -12
- package/src/ui/components/PromptDialog.test.tsx +4 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/primitives/Dialog.test.tsx +1 -0
- package/src/workbench/components/ChangesColumn.tsx +1 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +2 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +1 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +1 -0
- package/src/workbench/components/editor-grid.test.ts +11 -0
- package/src/workbench/components/editor-grid.ts +10 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { subscribeThreadState, connectThreadObserver } from "@cueloop/daemon/thread-subscription";
|
|
2
3
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
3
4
|
import type { ThreadAgentState } from "@cueloop/schema";
|
|
4
5
|
|
|
@@ -26,6 +27,7 @@ export function useThreadAgent(id: string, home?: string, injected?: ThreadAgent
|
|
|
26
27
|
let cancelled = false;
|
|
27
28
|
let connection: DaemonClient | undefined;
|
|
28
29
|
let unsubscribe: (() => void) | undefined;
|
|
30
|
+
|
|
29
31
|
revision.current++;
|
|
30
32
|
const refresh = async (api: ThreadAgentClient) => {
|
|
31
33
|
const requested = ++revision.current;
|
|
@@ -51,21 +53,29 @@ export function useThreadAgent(id: string, home?: string, injected?: ThreadAgent
|
|
|
51
53
|
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
unsubscribe = subscribeThreadState({
|
|
57
|
+
connect: connectThreadObserver({ home, autostart: true }),
|
|
58
|
+
matches: (event) => event.event === "agent.updated" && event.sessionId === id,
|
|
59
|
+
read: async (api, signal) => {
|
|
60
|
+
const requested = ++revision.current;
|
|
61
|
+
const state = await api.agentGet(id, { signal });
|
|
62
|
+
|
|
63
|
+
return { state, requested };
|
|
64
|
+
},
|
|
65
|
+
onConnect: (api) => {
|
|
66
|
+
connection = api;
|
|
67
|
+
setClient(api);
|
|
68
|
+
void configure(api).catch((error) => {
|
|
63
69
|
if (!cancelled) setError(String(error));
|
|
64
70
|
});
|
|
71
|
+
},
|
|
72
|
+
onValue: ({ state, requested }) => {
|
|
73
|
+
if (!cancelled && requested === revision.current) setState(state);
|
|
74
|
+
},
|
|
75
|
+
onError: (error) => {
|
|
76
|
+
if (!cancelled) setError(String(error));
|
|
77
|
+
},
|
|
65
78
|
});
|
|
66
|
-
await connection.subscribe();
|
|
67
|
-
await refresh(connection);
|
|
68
|
-
await configure(connection);
|
|
69
79
|
} catch (error) {
|
|
70
80
|
if (!cancelled) setError(error instanceof Error ? error.message : String(error));
|
|
71
81
|
}
|
|
@@ -21,6 +21,7 @@ test("the input is focused on open, so typing lands in the dialog", async () =>
|
|
|
21
21
|
/>,
|
|
22
22
|
{ width: 60, height: 12 },
|
|
23
23
|
);
|
|
24
|
+
|
|
24
25
|
await settle(setup);
|
|
25
26
|
|
|
26
27
|
await typeText(setup, "!");
|
|
@@ -45,14 +46,17 @@ test("clicking save fires onSave and clicking cancel fires onCancel", async () =
|
|
|
45
46
|
/>,
|
|
46
47
|
{ width: 60, height: 12 },
|
|
47
48
|
);
|
|
49
|
+
|
|
48
50
|
await settle(setup);
|
|
49
51
|
|
|
50
52
|
const save = locateText(setup, "save");
|
|
53
|
+
|
|
51
54
|
await setup.mockMouse.click(save.column, save.row);
|
|
52
55
|
expect(saved).toBe(1);
|
|
53
56
|
expect(cancelled).toBe(0);
|
|
54
57
|
|
|
55
58
|
const cancel = locateText(setup, "cancel");
|
|
59
|
+
|
|
56
60
|
await setup.mockMouse.click(cancel.column, cancel.row);
|
|
57
61
|
expect(cancelled).toBe(1);
|
|
58
62
|
expect(saved).toBe(1);
|
|
@@ -140,6 +140,7 @@ export function ChangesFileTree({
|
|
|
140
140
|
return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
|
|
141
141
|
if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
|
|
142
142
|
const row = rows[cursorIndex];
|
|
143
|
+
|
|
143
144
|
if (!row) return;
|
|
144
145
|
if (key.name === "return" || key.name === "enter")
|
|
145
146
|
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
@@ -41,6 +41,7 @@ test("j moves onto a file and Enter opens it", async () => {
|
|
|
41
41
|
/>,
|
|
42
42
|
{ width: 40, height: 12 },
|
|
43
43
|
);
|
|
44
|
+
|
|
44
45
|
await settle(setup);
|
|
45
46
|
await waitForText(setup, "a.ts");
|
|
46
47
|
|
|
@@ -117,6 +118,7 @@ test("without focus the keyboard opens nothing", async () => {
|
|
|
117
118
|
<ChangesFileTree files={files} onSelectFile={(path) => opened.push(path)} theme={DARK} />,
|
|
118
119
|
{ width: 40, height: 12 },
|
|
119
120
|
);
|
|
121
|
+
|
|
120
122
|
await settle(setup);
|
|
121
123
|
await waitForText(setup, "a.ts");
|
|
122
124
|
|
|
@@ -34,12 +34,14 @@ export function FileContentsView({
|
|
|
34
34
|
// instance; a state-backed ref rebinds the target once the code renderable exists.
|
|
35
35
|
const [codeTarget, setCodeTarget] = useState<CodeRenderable | null>(null);
|
|
36
36
|
const loadRef = useRef(loadContents);
|
|
37
|
+
|
|
37
38
|
useEffect(() => {
|
|
38
39
|
loadRef.current = loadContents;
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
useEffect(() => {
|
|
42
43
|
let alive = true;
|
|
44
|
+
|
|
43
45
|
void loadRef.current(path).then(
|
|
44
46
|
(contents) => {
|
|
45
47
|
if (alive) setLoaded({ path, lines: contents === null ? null : contents.split("\n") });
|
|
@@ -31,6 +31,7 @@ test("Enter expands a folder, then j moves down and Enter opens the file", async
|
|
|
31
31
|
/>,
|
|
32
32
|
{ width: 40, height: 12 },
|
|
33
33
|
);
|
|
34
|
+
|
|
34
35
|
await settle(setup);
|
|
35
36
|
await waitForText(setup, "src");
|
|
36
37
|
|
|
@@ -144,6 +145,7 @@ test("without focus the keyboard does nothing", async () => {
|
|
|
144
145
|
/>,
|
|
145
146
|
{ width: 40, height: 12 },
|
|
146
147
|
);
|
|
148
|
+
|
|
147
149
|
await settle(setup);
|
|
148
150
|
await waitForText(setup, "src");
|
|
149
151
|
|
|
@@ -223,6 +225,7 @@ test("a double-click requests a persistent file tab", async () => {
|
|
|
223
225
|
|
|
224
226
|
await waitForText(setup, "README.md");
|
|
225
227
|
const file = locateText(setup, "README.md");
|
|
228
|
+
|
|
226
229
|
await setup.mockMouse.doubleClick(file.column, file.row);
|
|
227
230
|
expect(opened).toEqual([
|
|
228
231
|
{ path: "README.md", persistent: false },
|
|
@@ -56,6 +56,7 @@ export function ProjectTreeView({
|
|
|
56
56
|
if (key.name === "k" || key.name === "up")
|
|
57
57
|
return setCursorId(rows[Math.max(cursorIndex - 1, 0)]!.id);
|
|
58
58
|
const row = rows[cursorIndex];
|
|
59
|
+
|
|
59
60
|
if (!row) return;
|
|
60
61
|
if (key.name === "return" || key.name === "enter")
|
|
61
62
|
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
@@ -19,10 +19,12 @@ import {
|
|
|
19
19
|
|
|
20
20
|
function asBranch(node: EditorNode | null): EditorBranch {
|
|
21
21
|
if (node === null || node.type !== "branch") throw new Error("expected a branch node");
|
|
22
|
+
|
|
22
23
|
return node;
|
|
23
24
|
}
|
|
24
25
|
function asGroup(node: EditorNode | null): EditorGroup {
|
|
25
26
|
if (node === null || node.type !== "group") throw new Error("expected a group node");
|
|
27
|
+
|
|
26
28
|
return node;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,6 +32,7 @@ describe("editor grid", () => {
|
|
|
30
32
|
test("a fresh group holds its tabs with the first active", () => {
|
|
31
33
|
const tab = changesTab();
|
|
32
34
|
const group = makeGroup([tab]);
|
|
35
|
+
|
|
33
36
|
expect(group.type).toBe("group");
|
|
34
37
|
expect(group.activeTabId).toBe(tab.id);
|
|
35
38
|
});
|
|
@@ -38,6 +41,7 @@ describe("editor grid", () => {
|
|
|
38
41
|
const group = makeGroup([changesTab()]);
|
|
39
42
|
const file = fileTab("App.tsx", "src/App.tsx", "diff");
|
|
40
43
|
const next = asGroup(addTab(group, group.id, file));
|
|
44
|
+
|
|
41
45
|
expect(next.tabs).toHaveLength(2);
|
|
42
46
|
expect(next.activeTabId).toBe(file.id);
|
|
43
47
|
});
|
|
@@ -81,8 +85,10 @@ describe("editor grid", () => {
|
|
|
81
85
|
test("splitting right makes a horizontal branch and focuses the new group", () => {
|
|
82
86
|
const group = makeGroup([fileTab("App.tsx", "src/App.tsx", "diff")]);
|
|
83
87
|
const { tree, focusGroupId } = splitGroup(group, group.id, "right");
|
|
88
|
+
|
|
84
89
|
expect(tree.type).toBe("branch");
|
|
85
90
|
const branch = asBranch(tree);
|
|
91
|
+
|
|
86
92
|
expect(branch.orientation).toBe("horizontal");
|
|
87
93
|
expect(branch.children).toHaveLength(2);
|
|
88
94
|
// the new group is the second child (right edge) and takes focus
|
|
@@ -95,6 +101,7 @@ describe("editor grid", () => {
|
|
|
95
101
|
const group = makeGroup([fileTab("a.ts", "a.ts", "contents")]);
|
|
96
102
|
const { tree, focusGroupId } = splitGroup(group, group.id, "up");
|
|
97
103
|
const branch = asBranch(tree);
|
|
104
|
+
|
|
98
105
|
expect(branch.orientation).toBe("vertical");
|
|
99
106
|
expect(branch.children[0]!.id).toBe(focusGroupId);
|
|
100
107
|
});
|
|
@@ -131,6 +138,7 @@ describe("editor grid", () => {
|
|
|
131
138
|
const branch = asBranch(tree);
|
|
132
139
|
const rightGroup = asGroup(branch.children[1]!);
|
|
133
140
|
const collapsed = closeTab(tree, rightGroup.id, rightGroup.tabs[0]!.id);
|
|
141
|
+
|
|
134
142
|
expect(collapsed?.type).toBe("group");
|
|
135
143
|
expect(asGroup(collapsed).id).toBe(group.id);
|
|
136
144
|
});
|
|
@@ -138,6 +146,7 @@ describe("editor grid", () => {
|
|
|
138
146
|
test("closing the only tab of the whole grid returns null", () => {
|
|
139
147
|
const tab = changesTab();
|
|
140
148
|
const group = makeGroup([tab]);
|
|
149
|
+
|
|
141
150
|
expect(closeTab(group, group.id, tab.id)).toBeNull();
|
|
142
151
|
});
|
|
143
152
|
|
|
@@ -146,8 +155,10 @@ describe("editor grid", () => {
|
|
|
146
155
|
const { tree } = splitGroup(group, group.id, "right");
|
|
147
156
|
const branch = asBranch(tree);
|
|
148
157
|
const rightGroup = asGroup(branch.children[1]!);
|
|
158
|
+
|
|
149
159
|
expect(containsGroup(tree, group.id)).toBe(true);
|
|
150
160
|
const collapsed = closeTab(tree, rightGroup.id, rightGroup.tabs[0]!.id);
|
|
161
|
+
|
|
151
162
|
expect(containsGroup(asGroup(collapsed), rightGroup.id)).toBe(false);
|
|
152
163
|
});
|
|
153
164
|
|
|
@@ -81,6 +81,7 @@ export function firstGroupId(node: EditorNode): string {
|
|
|
81
81
|
/** Whether a group id still exists in the grid; used to re-home focus after a prune. */
|
|
82
82
|
export function containsGroup(node: EditorNode, groupId: string): boolean {
|
|
83
83
|
if (node.type === "group") return node.id === groupId;
|
|
84
|
+
|
|
84
85
|
return node.children.some((child) => containsGroup(child, groupId));
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -97,6 +98,7 @@ function replaceGroup(
|
|
|
97
98
|
fn: (group: EditorGroup) => EditorNode,
|
|
98
99
|
): EditorNode {
|
|
99
100
|
if (node.type === "group") return node.id === id ? fn(node) : node;
|
|
101
|
+
|
|
100
102
|
return { ...node, children: node.children.map((child) => replaceGroup(child, id, fn)) };
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -223,8 +225,10 @@ export function keepFileTab(node: EditorNode, groupId: string, tabId: string): E
|
|
|
223
225
|
export function pruneEmpty(node: EditorNode): EditorNode | null {
|
|
224
226
|
if (node.type === "group") return node.tabs.length > 0 ? node : null;
|
|
225
227
|
const kept = node.children.map(pruneEmpty).filter((child): child is EditorNode => child !== null);
|
|
228
|
+
|
|
226
229
|
if (kept.length === 0) return null;
|
|
227
230
|
if (kept.length === 1) return kept[0]!;
|
|
231
|
+
|
|
228
232
|
return { ...node, children: kept };
|
|
229
233
|
}
|
|
230
234
|
|
|
@@ -233,8 +237,10 @@ export function closeTab(node: EditorNode, groupId: string, tabId: string): Edit
|
|
|
233
237
|
const next = replaceGroup(node, groupId, (group) => {
|
|
234
238
|
const tabs = group.tabs.filter((tab) => tab.id !== tabId);
|
|
235
239
|
const activeTabId = group.activeTabId === tabId ? (tabs[0]?.id ?? null) : group.activeTabId;
|
|
240
|
+
|
|
236
241
|
return { ...group, tabs, activeTabId };
|
|
237
242
|
});
|
|
243
|
+
|
|
238
244
|
return pruneEmpty(next);
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -249,10 +255,12 @@ export function splitGroup(
|
|
|
249
255
|
let focusGroupId = groupId;
|
|
250
256
|
const tree = replaceGroup(node, groupId, (group) => {
|
|
251
257
|
const active = group.tabs.find((tab) => tab.id === group.activeTabId) ?? group.tabs[0];
|
|
258
|
+
|
|
252
259
|
if (active === undefined) return group;
|
|
253
260
|
const movedTab: EditorTab =
|
|
254
261
|
active.kind === "welcome" ? { ...active, id: freshId() } : { ...active, preview: false };
|
|
255
262
|
const newGroup = makeGroup([movedTab]);
|
|
263
|
+
|
|
256
264
|
focusGroupId = newGroup.id;
|
|
257
265
|
const orientation = direction === "left" || direction === "right" ? "horizontal" : "vertical";
|
|
258
266
|
const newFirst = direction === "left" || direction === "up";
|
|
@@ -268,7 +276,9 @@ export function splitGroup(
|
|
|
268
276
|
activeTabId: sourceTabs[0]!.id,
|
|
269
277
|
};
|
|
270
278
|
const children = newFirst ? [newGroup, sourceGroup] : [sourceGroup, newGroup];
|
|
279
|
+
|
|
271
280
|
return { type: "branch", id: freshId(), orientation, children };
|
|
272
281
|
});
|
|
282
|
+
|
|
273
283
|
return { tree, focusGroupId };
|
|
274
284
|
}
|
|
@@ -141,24 +141,29 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
|
|
|
141
141
|
const close = (groupId: string, tabId: string): void => {
|
|
142
142
|
setGrid((tree) => {
|
|
143
143
|
const pruned = closeTab(tree, groupId, tabId);
|
|
144
|
+
|
|
144
145
|
if (pruned === null) {
|
|
145
146
|
setChangesOpen(false);
|
|
146
147
|
setProjectMode("tree");
|
|
147
148
|
// leaving zoom on would hide the Thread pane with no editor to fill the gap
|
|
148
149
|
setZoomed(false);
|
|
150
|
+
|
|
149
151
|
return makeGroup([changesTab()]);
|
|
150
152
|
}
|
|
151
153
|
// re-home focus when the closed group was pruned away, so the next open has a live target
|
|
152
154
|
setFocusedGroup((current) =>
|
|
153
155
|
current !== null && containsGroup(pruned, current) ? current : firstGroupId(pruned),
|
|
154
156
|
);
|
|
157
|
+
|
|
155
158
|
return pruned;
|
|
156
159
|
});
|
|
157
160
|
};
|
|
158
161
|
const split = (groupId: string, direction: SplitDirection): void => {
|
|
159
162
|
setGrid((tree) => {
|
|
160
163
|
const result = splitGroup(tree, groupId, direction);
|
|
164
|
+
|
|
161
165
|
setFocusedGroup(result.focusGroupId);
|
|
166
|
+
|
|
162
167
|
return result.tree;
|
|
163
168
|
});
|
|
164
169
|
};
|