@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.78
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 +3 -3
- package/src/App.diff.test.tsx +14 -2
- package/src/App.export.test.tsx +1 -2
- package/src/App.plan-review.test.tsx +16 -0
- package/src/App.submit-confirm.test.tsx +34 -5
- package/src/App.test.tsx +2 -2
- package/src/App.tsx +4 -0
- package/src/App.walk.test.tsx +2 -2
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +7 -3
- package/src/app-view-model.ts +3 -0
- package/src/components/AnnotatableFileView.tsx +1 -1
- package/src/components/AnnotationCards.tsx +18 -2
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +4 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/ManageAccessDialog.tsx +13 -11
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/ShareChoiceDialog.tsx +0 -2
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadTree.tsx +11 -6
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -1
- package/src/components/ThreadView.tsx +66 -7
- package/src/components/Toast.tsx +0 -5
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +219 -89
- package/src/components/primitives/Card.tsx +3 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +8 -1
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -1
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/run.ts +31 -19
- package/src/serve.ts +1 -1
- package/src/share.ts +1 -1
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.switch-cache.test.ts +146 -0
- package/src/thread-controller.ts +157 -24
- package/src/use-annotation-surface.tsx +43 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.78",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
33
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.78",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.78",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.78",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.78",
|
|
34
34
|
"@opentui/core": "^0.5.8",
|
|
35
35
|
"@opentui/keymap": "0.5.8",
|
|
36
36
|
"@opentui/react": "^0.5.8",
|
|
@@ -57,7 +57,7 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
57
57
|
|
|
58
58
|
async function submitApprove(setup: Setup): Promise<void> {
|
|
59
59
|
await pressKey(setup, "RETURN", { meta: true }); // open submit (approve default: no pending items)
|
|
60
|
-
await
|
|
60
|
+
await pressKey(setup, "RETURN", { meta: true }); // confirm
|
|
61
61
|
await waitForText(setup, "review approved");
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -74,8 +74,8 @@ describe("completion overlay", () => {
|
|
|
74
74
|
const frame = await waitForText(setup, "closing in 5s");
|
|
75
75
|
|
|
76
76
|
expect(frame).toContain("review approved");
|
|
77
|
-
expect(frame).toContain("close
|
|
78
|
-
expect(frame).toContain("
|
|
77
|
+
expect(frame).toContain("close");
|
|
78
|
+
expect(frame).toContain("back to plan");
|
|
79
79
|
|
|
80
80
|
// Act
|
|
81
81
|
await press(setup, "enter");
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
clickText,
|
|
14
14
|
dragText,
|
|
15
15
|
isolateUserConfig,
|
|
16
|
-
press,
|
|
17
16
|
pressKey,
|
|
18
17
|
renderReadyApp,
|
|
19
18
|
typeText,
|
|
@@ -95,7 +94,7 @@ describe("diff review", () => {
|
|
|
95
94
|
|
|
96
95
|
// Act - submit with the session chord (cmd+enter, no composer open), confirm request_changes
|
|
97
96
|
await pressKey(setup, "RETURN", { meta: true });
|
|
98
|
-
await
|
|
97
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
99
98
|
|
|
100
99
|
// Assert
|
|
101
100
|
await waitForText(setup, "feedback sent");
|
|
@@ -105,6 +104,19 @@ describe("diff review", () => {
|
|
|
105
104
|
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
106
105
|
});
|
|
107
106
|
|
|
107
|
+
test("pasting an image into a diff comment drops in an [Image #n] placeholder", async () => {
|
|
108
|
+
// Arrange - mark the added line and open the inline comment composer
|
|
109
|
+
const setup = await renderApp();
|
|
110
|
+
await dragText(setup, "new Map()", "new Map()", "new Map()".length);
|
|
111
|
+
await typeText(setup, "see ");
|
|
112
|
+
|
|
113
|
+
// Act - a burst of control bytes stands in for the binary an image paste delivers
|
|
114
|
+
await setup.mockInput.pasteBracketedText("");
|
|
115
|
+
|
|
116
|
+
// Assert - the same placeholder the summary composer shows
|
|
117
|
+
await waitForText(setup, "see [Image #1]");
|
|
118
|
+
});
|
|
119
|
+
|
|
108
120
|
test("a comment reopens with its mark painted on the code", async () => {
|
|
109
121
|
// Arrange - a stored comment anchored to the added line's words
|
|
110
122
|
server.core.sessionAnnotate(session.id, {
|
package/src/App.export.test.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import type { Thread } from "@cueloop/schema";
|
|
|
18
18
|
import { App } from "./App";
|
|
19
19
|
import {
|
|
20
20
|
isolateUserConfig,
|
|
21
|
-
press,
|
|
22
21
|
waitForState,
|
|
23
22
|
waitForText,
|
|
24
23
|
pressKey,
|
|
@@ -81,7 +80,7 @@ describe("obsidian export on resolve", () => {
|
|
|
81
80
|
await waitForText(setup, "[Approve]");
|
|
82
81
|
|
|
83
82
|
// Act
|
|
84
|
-
await
|
|
83
|
+
await pressKey(setup, "RETURN", { meta: true }); // submit
|
|
85
84
|
|
|
86
85
|
// Assert
|
|
87
86
|
// the export is an async round-trip after resolve; the status line lands
|
|
@@ -228,6 +228,22 @@ describe("the mark stays painted while composing", () => {
|
|
|
228
228
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
229
229
|
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
230
230
|
}, 60_000);
|
|
231
|
+
|
|
232
|
+
test("backspace on an empty draft dismisses the composer back to the mark", async () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
const setup = await renderApp();
|
|
235
|
+
|
|
236
|
+
// Act: mark, type one character, then delete it and backspace again on the now-empty draft
|
|
237
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
238
|
+
await type(setup, "x");
|
|
239
|
+
await waitForText(setup, "● x");
|
|
240
|
+
await press(setup, "backspace");
|
|
241
|
+
await press(setup, "backspace");
|
|
242
|
+
|
|
243
|
+
// Assert: the composer is gone and the mark stays, ready to re-type
|
|
244
|
+
await waitForTextGone(setup, "● x");
|
|
245
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
246
|
+
}, 60_000);
|
|
231
247
|
});
|
|
232
248
|
|
|
233
249
|
describe("compose newline convention", () => {
|
|
@@ -99,8 +99,8 @@ describe("send message confirm", () => {
|
|
|
99
99
|
const frame = setup.captureCharFrame();
|
|
100
100
|
|
|
101
101
|
expect(frame).toContain("[Approve]");
|
|
102
|
-
expect(frame).toContain(" send
|
|
103
|
-
expect(frame).toContain("
|
|
102
|
+
expect(frame).toContain(" send ");
|
|
103
|
+
expect(frame).toContain(" cancel ");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test("left/right cycles the verdict selector in the overlay", async () => {
|
|
@@ -146,7 +146,7 @@ describe("send message confirm", () => {
|
|
|
146
146
|
// Assert - a bare ESC settles after the parser's escape-sequence window
|
|
147
147
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
148
148
|
|
|
149
|
-
expect(frame).not.toContain("
|
|
149
|
+
expect(frame).not.toContain(" cancel ");
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
test("enter in the overlay resolves the session through the controller", async () => {
|
|
@@ -162,7 +162,7 @@ describe("send message confirm", () => {
|
|
|
162
162
|
|
|
163
163
|
// Act
|
|
164
164
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
165
|
-
await
|
|
165
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
166
166
|
|
|
167
167
|
// Assert - the completion flow after submit is unchanged
|
|
168
168
|
await waitForText(setup, "feedback sent");
|
|
@@ -172,6 +172,35 @@ describe("send message confirm", () => {
|
|
|
172
172
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test("typing / in the summary opens the skills/actions palette", async () => {
|
|
176
|
+
// Arrange
|
|
177
|
+
seedAnnotations(1);
|
|
178
|
+
const setup = await renderApp();
|
|
179
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
180
|
+
await waitForText(setup, "[Changes]");
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
await setup.mockInput.typeText("/restate");
|
|
184
|
+
|
|
185
|
+
// Assert - the same slash palette the inline composer shows
|
|
186
|
+
await waitForText(setup, "restate-simplified");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const setup = await renderApp();
|
|
192
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
193
|
+
await waitForText(setup, "[Approve]");
|
|
194
|
+
|
|
195
|
+
// Act - a burst of control bytes stands in for the binary an image paste delivers
|
|
196
|
+
await setup.mockInput.typeText("look at this ");
|
|
197
|
+
await setup.mockInput.pasteBracketedText("");
|
|
198
|
+
|
|
199
|
+
// Assert - the raw bytes never reach the draft; a numbered placeholder does
|
|
200
|
+
await waitForText(setup, "[Image #1]");
|
|
201
|
+
expect(setup.captureCharFrame()).toContain("look at this [Image #1]");
|
|
202
|
+
});
|
|
203
|
+
|
|
175
204
|
test("read-only observers cannot open the confirm overlay", async () => {
|
|
176
205
|
// Arrange
|
|
177
206
|
seedAnnotations(1);
|
|
@@ -185,7 +214,7 @@ describe("send message confirm", () => {
|
|
|
185
214
|
const frame = setup.captureCharFrame();
|
|
186
215
|
|
|
187
216
|
expect(frame).not.toContain("[Changes]");
|
|
188
|
-
expect(frame).not.toContain("
|
|
217
|
+
expect(frame).not.toContain(" cancel ");
|
|
189
218
|
expect(server.core.sessionGet(session.id).status).toBe("pending");
|
|
190
219
|
});
|
|
191
220
|
});
|
package/src/App.test.tsx
CHANGED
|
@@ -207,7 +207,7 @@ describe("submit", () => {
|
|
|
207
207
|
|
|
208
208
|
// Act
|
|
209
209
|
await type(setup, "Expand the steps.");
|
|
210
|
-
await
|
|
210
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
211
211
|
|
|
212
212
|
// Assert
|
|
213
213
|
await waitForText(setup, "feedback sent");
|
|
@@ -231,7 +231,7 @@ describe("submit", () => {
|
|
|
231
231
|
await waitForText(setup, "[Approve]"); // no pending items → approve default
|
|
232
232
|
|
|
233
233
|
// Act
|
|
234
|
-
await
|
|
234
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
235
235
|
|
|
236
236
|
// Assert
|
|
237
237
|
await waitForState(setup, () => server.core.sessionGet(session.id).verdict !== undefined);
|
package/src/App.tsx
CHANGED
|
@@ -1068,6 +1068,9 @@ export function App({
|
|
|
1068
1068
|
verdict={activeSession.verdict.kind}
|
|
1069
1069
|
completion={completion}
|
|
1070
1070
|
status={status}
|
|
1071
|
+
onClose={() => dispatch({ type: "finishReview" })}
|
|
1072
|
+
onBackToPlan={() => dispatch({ type: "dismissCompletion" })}
|
|
1073
|
+
onAlways={() => dispatch({ type: "optInAutoClose" })}
|
|
1071
1074
|
/>
|
|
1072
1075
|
);
|
|
1073
1076
|
|
|
@@ -1086,6 +1089,7 @@ export function App({
|
|
|
1086
1089
|
session: activeSession,
|
|
1087
1090
|
walkFileList,
|
|
1088
1091
|
viewedPaths,
|
|
1092
|
+
quickActions,
|
|
1089
1093
|
liveInput,
|
|
1090
1094
|
setMode,
|
|
1091
1095
|
dispatch,
|
package/src/App.walk.test.tsx
CHANGED
|
@@ -162,7 +162,7 @@ describe("the guided walk", () => {
|
|
|
162
162
|
expect(setup.captureCharFrame()).toContain("3/3 files viewed");
|
|
163
163
|
|
|
164
164
|
// Act
|
|
165
|
-
await
|
|
165
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
166
166
|
|
|
167
167
|
// Assert
|
|
168
168
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -253,7 +253,7 @@ describe("the guided walk", () => {
|
|
|
253
253
|
await pressKey(setup, "RETURN", { meta: true });
|
|
254
254
|
// nothing pending despite the note, so the confirm card defaults to approve
|
|
255
255
|
await waitForText(setup, "[Approve]");
|
|
256
|
-
await
|
|
256
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
257
257
|
|
|
258
258
|
// Assert
|
|
259
259
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ansiScreenToLines } from "./ansi-screen";
|
|
3
|
+
|
|
4
|
+
describe("ansiScreenToLines", () => {
|
|
5
|
+
test("places text at the positioned cursor", () => {
|
|
6
|
+
const lines = ansiScreenToLines("\x1b[2;3Hhi", 10, 4);
|
|
7
|
+
|
|
8
|
+
expect(lines[0]).toBe("");
|
|
9
|
+
expect(lines[1]).toBe(" hi");
|
|
10
|
+
expect(lines[2]).toBe("");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("clears the screen before repositioning", () => {
|
|
14
|
+
const lines = ansiScreenToLines("\x1b[1;1Hold\x1b[2J\x1b[1;1Hnew", 10, 2);
|
|
15
|
+
|
|
16
|
+
expect(lines[0]).toBe("new");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("ignores colour, cursor-visibility, and clipboard sequences", () => {
|
|
20
|
+
const lines = ansiScreenToLines("\x1b[?25l\x1b[1;1H\x1b[32mgo\x1b]52;c;AA\x07", 10, 2);
|
|
21
|
+
|
|
22
|
+
expect(lines[0]).toBe("go");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("clamps writes outside the grid instead of throwing", () => {
|
|
26
|
+
expect(() => ansiScreenToLines("\x1b[99;99Hx", 5, 3)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Interpret an absolute-positioned ANSI byte stream (cursor moves plus printed text) into a grid of rows. */
|
|
2
|
+
|
|
3
|
+
const CSI = "\x1b[";
|
|
4
|
+
|
|
5
|
+
export function ansiScreenToLines(ansi: string, cols: number, rows: number): string[] {
|
|
6
|
+
const grid: string[][] = Array.from({ length: rows }, () =>
|
|
7
|
+
Array.from({ length: cols }, () => " "),
|
|
8
|
+
);
|
|
9
|
+
let row = 0;
|
|
10
|
+
let column = 0;
|
|
11
|
+
let index = 0;
|
|
12
|
+
|
|
13
|
+
const put = (character: string): void => {
|
|
14
|
+
if (row >= 0 && row < rows && column >= 0 && column < cols) grid[row]![column] = character;
|
|
15
|
+
column += 1;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
while (index < ansi.length) {
|
|
19
|
+
const character = ansi[index]!;
|
|
20
|
+
|
|
21
|
+
if (character === "\x1b") {
|
|
22
|
+
index = consumeEscape(ansi, index, (nextRow, nextColumn) => {
|
|
23
|
+
if (nextRow === "clear") {
|
|
24
|
+
for (const line of grid) line.fill(" ");
|
|
25
|
+
row = 0;
|
|
26
|
+
column = 0;
|
|
27
|
+
} else {
|
|
28
|
+
row = nextRow;
|
|
29
|
+
column = nextColumn;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (character === "\n") {
|
|
35
|
+
row += 1;
|
|
36
|
+
column = 0;
|
|
37
|
+
} else if (character === "\r") {
|
|
38
|
+
column = 0;
|
|
39
|
+
} else {
|
|
40
|
+
put(character);
|
|
41
|
+
}
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return grid.map((line) => line.join("").replace(/\s+$/, ""));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type EscapeEffect = (row: number | "clear", column: number) => void;
|
|
49
|
+
|
|
50
|
+
function consumeEscape(ansi: string, start: number, apply: EscapeEffect): number {
|
|
51
|
+
if (ansi[start + 1] === "]") {
|
|
52
|
+
const bell = ansi.indexOf("\x07", start);
|
|
53
|
+
|
|
54
|
+
return bell === -1 ? ansi.length : bell + 1;
|
|
55
|
+
}
|
|
56
|
+
if (ansi.slice(start, start + CSI.length) !== CSI) return start + 1;
|
|
57
|
+
let index = start + CSI.length;
|
|
58
|
+
|
|
59
|
+
while (index < ansi.length && !/[A-Za-z]/.test(ansi[index]!)) index += 1;
|
|
60
|
+
const final = ansi[index];
|
|
61
|
+
const parameters = ansi.slice(start + CSI.length, index);
|
|
62
|
+
|
|
63
|
+
if (final === "H" || final === "f") {
|
|
64
|
+
const [rowParameter = "1", columnParameter = "1"] = parameters.split(";");
|
|
65
|
+
|
|
66
|
+
apply(Math.max(0, Number(rowParameter) - 1), Math.max(0, Number(columnParameter) - 1));
|
|
67
|
+
} else if (final === "J" && parameters === "2") {
|
|
68
|
+
apply("clear", 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return index + 1;
|
|
72
|
+
}
|
package/src/app-screens.tsx
CHANGED
|
@@ -388,7 +388,6 @@ export function NoThreadShell(props: {
|
|
|
388
388
|
<PromptDialog
|
|
389
389
|
isOpen
|
|
390
390
|
title=" rename thread "
|
|
391
|
-
label="new title for this thread:"
|
|
392
391
|
value={mode.text}
|
|
393
392
|
placeholder="a short title"
|
|
394
393
|
onInput={(text) => setMode({ ...mode, text })}
|
|
@@ -412,8 +411,11 @@ export function CompletionScreen(props: {
|
|
|
412
411
|
verdict: VerdictKind;
|
|
413
412
|
completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
|
|
414
413
|
status: string;
|
|
414
|
+
onClose: () => void;
|
|
415
|
+
onBackToPlan: () => void;
|
|
416
|
+
onAlways: () => void;
|
|
415
417
|
}): React.ReactNode {
|
|
416
|
-
const { theme, session, verdict, completion, status } = props;
|
|
418
|
+
const { theme, session, verdict, completion, status, onClose, onBackToPlan, onAlways } = props;
|
|
417
419
|
|
|
418
420
|
return (
|
|
419
421
|
<ThemeProvider theme={theme}>
|
|
@@ -421,6 +423,9 @@ export function CompletionScreen(props: {
|
|
|
421
423
|
verdict={verdict}
|
|
422
424
|
completion={completion}
|
|
423
425
|
status={status}
|
|
426
|
+
onClose={onClose}
|
|
427
|
+
onBackToPlan={onBackToPlan}
|
|
428
|
+
onAlways={onAlways}
|
|
424
429
|
returnsTo={
|
|
425
430
|
returnPaneFor(session.artifact.meta.herdrPane)
|
|
426
431
|
? (session.artifact.meta.agent ?? "the agent")
|
|
@@ -510,7 +515,6 @@ export function TrailingOverlays(props: {
|
|
|
510
515
|
<PromptDialog
|
|
511
516
|
isOpen
|
|
512
517
|
title=" rename thread "
|
|
513
|
-
label="new title for this thread:"
|
|
514
518
|
value={mode.text}
|
|
515
519
|
placeholder="a short title"
|
|
516
520
|
onInput={(text) => setMode({ ...mode, text })}
|
package/src/app-view-model.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Mode } from "./intent-dispatch";
|
|
|
4
4
|
import type { Intent, KeyState } from "./keymap";
|
|
5
5
|
import type { Completion } from "./thread-controller";
|
|
6
6
|
import type { WalkFile } from "./walk";
|
|
7
|
+
import type { QuickAction } from "./config";
|
|
7
8
|
import { viewedCount } from "./walk";
|
|
8
9
|
import type { ConfirmCardProps } from "./components/ConfirmCard";
|
|
9
10
|
import type { BreadcrumbItem } from "./components/Breadcrumb";
|
|
@@ -136,6 +137,7 @@ export function buildSubmitConfirmState(
|
|
|
136
137
|
session: Thread;
|
|
137
138
|
walkFileList: WalkFile[];
|
|
138
139
|
viewedPaths: Set<string>;
|
|
140
|
+
quickActions: QuickAction[];
|
|
139
141
|
},
|
|
140
142
|
): Omit<ConfirmCardProps, "theme"> | null {
|
|
141
143
|
const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
|
|
@@ -145,6 +147,7 @@ export function buildSubmitConfirmState(
|
|
|
145
147
|
return {
|
|
146
148
|
verdict: mode.verdict,
|
|
147
149
|
summary: mode.summary,
|
|
150
|
+
quickActions: deps.quickActions,
|
|
148
151
|
viewedSummary:
|
|
149
152
|
isDiff && session.viewedPaths !== undefined
|
|
150
153
|
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A project file shown as its own annotatable surface: the file's lines render through the diff
|
|
2
2
|
// sheet as context rows (one line-number gutter, no +/- sign), so selecting text leaves a comment
|
|
3
|
-
// anchored to the file exactly as a plan or diff comment is. Used where a
|
|
3
|
+
// anchored to the file exactly as a plan or diff comment is. Used where a thread exists to
|
|
4
4
|
// hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
|
|
5
5
|
|
|
6
6
|
import React, { useEffect, useRef, useState } from "react";
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useContext, useLayoutEffect, useRef, useState } from "react";
|
|
10
|
+
import { usePaste } from "@opentui/react";
|
|
10
11
|
import type {
|
|
11
12
|
BoxRenderable,
|
|
12
13
|
KeyBinding,
|
|
@@ -18,6 +19,7 @@ import type { Theme } from "../theme";
|
|
|
18
19
|
import { useTooltip } from "./Tooltip";
|
|
19
20
|
import { lighten } from "../annotation-palette";
|
|
20
21
|
import { useFrameMeasure } from "../use-frame-measure";
|
|
22
|
+
import { imagePlaceholder, looksLikeBinaryPaste } from "../pasted-image";
|
|
21
23
|
import { skillReferenceRanges, type SlashItem } from "../slash-palette";
|
|
22
24
|
import { PaletteNamesContext } from "../skills";
|
|
23
25
|
import { referenceStyleFor } from "./syntax-highlight";
|
|
@@ -35,7 +37,7 @@ const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
|
|
|
35
37
|
{ name: "return", shift: true, action: "newline" },
|
|
36
38
|
];
|
|
37
39
|
|
|
38
|
-
function composeRowCount(text: string, contentWidth: number): number {
|
|
40
|
+
export function composeRowCount(text: string, contentWidth: number): number {
|
|
39
41
|
const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
|
|
40
42
|
let visualRowCount = 0;
|
|
41
43
|
|
|
@@ -54,6 +56,7 @@ export function Composer({
|
|
|
54
56
|
onSave,
|
|
55
57
|
onReady,
|
|
56
58
|
onInput,
|
|
59
|
+
placeholder,
|
|
57
60
|
}: {
|
|
58
61
|
seed: string;
|
|
59
62
|
glyph: string;
|
|
@@ -61,12 +64,23 @@ export function Composer({
|
|
|
61
64
|
onSave: (body: string) => void;
|
|
62
65
|
onReady: () => void;
|
|
63
66
|
onInput: (text: string, caret: number) => void;
|
|
67
|
+
placeholder?: string;
|
|
64
68
|
}): React.ReactNode {
|
|
65
69
|
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
70
|
+
const pastedImageCount = useRef(0);
|
|
66
71
|
const [rows, setRows] = useState(1);
|
|
67
72
|
// action and skill names whose "/name" references paint in the accent color
|
|
68
73
|
const referenceNames = useContext(PaletteNamesContext);
|
|
69
74
|
|
|
75
|
+
usePaste((event) => {
|
|
76
|
+
const editor = editorRef.current;
|
|
77
|
+
|
|
78
|
+
if (!editor || !editor.focused || !looksLikeBinaryPaste(event.bytes)) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
pastedImageCount.current += 1;
|
|
81
|
+
editor.editBuffer.insertText(imagePlaceholder(pastedImageCount.current));
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
const paintReferences = (editor: TextareaRenderable): void => {
|
|
71
85
|
const { styleId } = referenceStyleFor(tokens);
|
|
72
86
|
|
|
@@ -98,6 +112,8 @@ export function Composer({
|
|
|
98
112
|
ref={editorRef}
|
|
99
113
|
focused
|
|
100
114
|
initialValue={seed}
|
|
115
|
+
placeholder={placeholder}
|
|
116
|
+
placeholderColor={tokens.textDim}
|
|
101
117
|
cursorStyle={{ style: "block", blinking: true }}
|
|
102
118
|
keyBindings={COMPOSE_KEY_BINDINGS}
|
|
103
119
|
onSubmit={() => onSave(editorRef.current?.plainText ?? "")}
|
|
@@ -247,7 +263,7 @@ export function DiscussionCard({
|
|
|
247
263
|
});
|
|
248
264
|
|
|
249
265
|
return (
|
|
250
|
-
<box style={{ flexDirection: "row", marginTop:
|
|
266
|
+
<box style={{ flexDirection: "row", marginTop: 0, marginLeft: 2 }}>
|
|
251
267
|
<text selectable={false} style={{ flexShrink: 0, width: 1 }}>
|
|
252
268
|
{edgeRows.map((row, index) => (
|
|
253
269
|
<span key={index} fg={row.color}>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Story, StoryMeta } from "./story";
|
|
3
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
4
|
+
|
|
5
|
+
export const meta: StoryMeta = { title: "Primitives/AnsiScreen" };
|
|
6
|
+
|
|
7
|
+
const POSITIONED = "\x1b[2;4HHello from\x1b[4;6Hraw, positioned ANSI";
|
|
8
|
+
|
|
9
|
+
export const Positioned: Story = {
|
|
10
|
+
render: () => <AnsiScreen ansi={POSITIONED} cols={40} rows={6} />,
|
|
11
|
+
size: { width: 44, height: 8 },
|
|
12
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Paints a raw ANSI screen (cursor-positioned text) as a terminal would, for previewing SSH surfaces. */
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { Theme } from "../theme";
|
|
5
|
+
import { ansiScreenToLines } from "../ansi-screen";
|
|
6
|
+
import { useComponentTheme } from "./theme-context";
|
|
7
|
+
|
|
8
|
+
export interface AnsiScreenProps {
|
|
9
|
+
ansi: string;
|
|
10
|
+
cols: number;
|
|
11
|
+
rows: number;
|
|
12
|
+
theme?: Theme;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AnsiScreen({ ansi, cols, rows, theme }: AnsiScreenProps): React.ReactNode {
|
|
16
|
+
const tokens = useComponentTheme(theme);
|
|
17
|
+
const lines = ansiScreenToLines(ansi, cols, rows);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<box style={{ flexDirection: "column" }}>
|
|
21
|
+
{lines.map((line, index) => (
|
|
22
|
+
<text key={index} fg={tokens.text} selectable={false} style={{ wrapMode: "none" }}>
|
|
23
|
+
{line === "" ? " " : line}
|
|
24
|
+
</text>
|
|
25
|
+
))}
|
|
26
|
+
</box>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -5,8 +5,19 @@ import { CompletionOverlay } from "./CompletionOverlay";
|
|
|
5
5
|
|
|
6
6
|
export const meta: StoryMeta = { title: "Overlays/CompletionOverlay" };
|
|
7
7
|
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
|
|
8
10
|
export const ApprovedPrompt: Story = {
|
|
9
|
-
render: () =>
|
|
11
|
+
render: () => (
|
|
12
|
+
<CompletionOverlay
|
|
13
|
+
verdict="approve"
|
|
14
|
+
completion={{ phase: "prompt" }}
|
|
15
|
+
status=""
|
|
16
|
+
onClose={noop}
|
|
17
|
+
onBackToPlan={noop}
|
|
18
|
+
onAlways={noop}
|
|
19
|
+
/>
|
|
20
|
+
),
|
|
10
21
|
expectedColors: [DARK.green],
|
|
11
22
|
};
|
|
12
23
|
|
|
@@ -17,6 +28,9 @@ export const FeedbackCounting: Story = {
|
|
|
17
28
|
completion={{ phase: "counting", remaining: 3 }}
|
|
18
29
|
status="exported to vault/cueloop/plan.md"
|
|
19
30
|
returnsTo="agent/worker-3"
|
|
31
|
+
onClose={noop}
|
|
32
|
+
onBackToPlan={noop}
|
|
33
|
+
onAlways={noop}
|
|
20
34
|
/>
|
|
21
35
|
),
|
|
22
36
|
expectedColors: [DARK.accent],
|
|
@@ -8,6 +8,8 @@ import React from "react";
|
|
|
8
8
|
import type { VerdictKind } from "@cueloop/schema";
|
|
9
9
|
import type { Theme } from "../theme";
|
|
10
10
|
import { useComponentTheme } from "./theme-context";
|
|
11
|
+
import { Toolbar } from "./primitives/Toolbar";
|
|
12
|
+
import { Button } from "./primitives/Button";
|
|
11
13
|
|
|
12
14
|
export interface CompletionOverlayProps {
|
|
13
15
|
verdict: VerdictKind;
|
|
@@ -16,6 +18,9 @@ export interface CompletionOverlayProps {
|
|
|
16
18
|
status: string;
|
|
17
19
|
/** Where focus goes on close (the agent's pane), when known. */
|
|
18
20
|
returnsTo?: string;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onBackToPlan: () => void;
|
|
23
|
+
onAlways: () => void;
|
|
19
24
|
theme?: Theme;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -24,6 +29,9 @@ export function CompletionOverlay({
|
|
|
24
29
|
completion,
|
|
25
30
|
status,
|
|
26
31
|
returnsTo,
|
|
32
|
+
onClose,
|
|
33
|
+
onBackToPlan,
|
|
34
|
+
onAlways,
|
|
27
35
|
theme,
|
|
28
36
|
}: CompletionOverlayProps): React.ReactNode {
|
|
29
37
|
const tokens = useComponentTheme(theme);
|
|
@@ -49,12 +57,21 @@ export function CompletionOverlay({
|
|
|
49
57
|
</text>
|
|
50
58
|
{returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
|
|
51
59
|
{status ? <text fg={tokens.textDim}>{status}</text> : null}
|
|
60
|
+
{completion.phase === "counting" ? (
|
|
61
|
+
<text fg={tokens.textDim}>closing in {completion.remaining}s</text>
|
|
62
|
+
) : null}
|
|
52
63
|
<text> </text>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
<Toolbar>
|
|
65
|
+
<Button variant="solid" marginRight={2} onPress={onClose} theme={theme}>
|
|
66
|
+
{" close "}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button marginRight={2} onPress={onBackToPlan} theme={theme}>
|
|
69
|
+
{" back to plan "}
|
|
70
|
+
</Button>
|
|
71
|
+
<Button onPress={onAlways} theme={theme}>
|
|
72
|
+
{" always "}
|
|
73
|
+
</Button>
|
|
74
|
+
</Toolbar>
|
|
58
75
|
</box>
|
|
59
76
|
);
|
|
60
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { DARK } from "../theme";
|
|
3
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
3
4
|
import type { Story, StoryMeta } from "./story";
|
|
4
5
|
import { ConfirmCard } from "./ConfirmCard";
|
|
5
6
|
|
|
@@ -10,6 +11,7 @@ const callbacks = {
|
|
|
10
11
|
onSelectVerdict: () => {},
|
|
11
12
|
onSubmit: () => {},
|
|
12
13
|
onCancel: () => {},
|
|
14
|
+
quickActions: DEFAULT_QUICK_ACTIONS,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const ApproveDefault: Story = {
|