@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.79
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 +14 -13
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +22 -5
- package/src/App.export.test.tsx +1 -2
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +91 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +39 -8
- package/src/App.test.tsx +33 -30
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +263 -274
- package/src/App.walk.test.tsx +11 -10
- package/src/App.workbench.test.tsx +86 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +1 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +19 -9
- package/src/app-view-model.ts +3 -1
- 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.stories.tsx +3 -3
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +18 -11
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +8 -2
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +12 -7
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -36
- package/src/components/ThreadView.tsx +200 -67
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -8
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +5 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +12 -4
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -2
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +39 -25
- package/src/serve.ts +1 -1
- package/src/share.ts +16 -2
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +386 -78
- package/src/use-annotation-surface.tsx +88 -63
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -120
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -82
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
package/src/App.walk.test.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { DARK } from "./theme";
|
|
|
12
12
|
import {
|
|
13
13
|
isolateUserConfig,
|
|
14
14
|
press,
|
|
15
|
+
navCommand,
|
|
15
16
|
pressKey,
|
|
16
17
|
renderReadyApp,
|
|
17
18
|
waitForState,
|
|
@@ -104,7 +105,7 @@ describe("the guided walk", () => {
|
|
|
104
105
|
const setup = await renderApp();
|
|
105
106
|
|
|
106
107
|
// Act
|
|
107
|
-
await
|
|
108
|
+
await navCommand(setup, "k");
|
|
108
109
|
|
|
109
110
|
// Assert
|
|
110
111
|
await waitForText(setup, "file 1 of 3 · 0 viewed");
|
|
@@ -162,7 +163,7 @@ describe("the guided walk", () => {
|
|
|
162
163
|
expect(setup.captureCharFrame()).toContain("3/3 files viewed");
|
|
163
164
|
|
|
164
165
|
// Act
|
|
165
|
-
await
|
|
166
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
166
167
|
|
|
167
168
|
// Assert
|
|
168
169
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -172,7 +173,7 @@ describe("the guided walk", () => {
|
|
|
172
173
|
// Arrange
|
|
173
174
|
const setup = await renderApp();
|
|
174
175
|
|
|
175
|
-
await
|
|
176
|
+
await navCommand(setup, "k");
|
|
176
177
|
await waitForText(setup, "file 1 of 3 · 0 viewed");
|
|
177
178
|
await press(setup, "]");
|
|
178
179
|
await waitForText(setup, "file 2 of 3 · 1 viewed");
|
|
@@ -186,7 +187,7 @@ describe("the guided walk", () => {
|
|
|
186
187
|
expect(setup.captureCharFrame()).not.toContain("· 1 viewed");
|
|
187
188
|
|
|
188
189
|
// Act
|
|
189
|
-
await
|
|
190
|
+
await navCommand(setup, "k");
|
|
190
191
|
|
|
191
192
|
// Assert
|
|
192
193
|
await waitForText(setup, "file 2 of 3 · 1 viewed");
|
|
@@ -196,7 +197,7 @@ describe("the guided walk", () => {
|
|
|
196
197
|
// Arrange
|
|
197
198
|
const first = await renderApp();
|
|
198
199
|
|
|
199
|
-
await
|
|
200
|
+
await navCommand(first, "k");
|
|
200
201
|
await waitForText(first, "file 1 of 3 · 0 viewed");
|
|
201
202
|
await press(first, "]");
|
|
202
203
|
await press(first, "]");
|
|
@@ -211,7 +212,7 @@ describe("the guided walk", () => {
|
|
|
211
212
|
// a fresh client reads the viewed set back from the session record
|
|
212
213
|
const second = await renderApp();
|
|
213
214
|
|
|
214
|
-
await
|
|
215
|
+
await navCommand(second, "k");
|
|
215
216
|
|
|
216
217
|
// Assert
|
|
217
218
|
await waitForText(second, "file 3 of 3 · 2 viewed");
|
|
@@ -232,7 +233,7 @@ describe("the guided walk", () => {
|
|
|
232
233
|
const setup = await renderApp();
|
|
233
234
|
|
|
234
235
|
// Act
|
|
235
|
-
await
|
|
236
|
+
await navCommand(setup, "k");
|
|
236
237
|
|
|
237
238
|
// Assert
|
|
238
239
|
await waitForText(setup, "file 1 of 3");
|
|
@@ -253,7 +254,7 @@ describe("the guided walk", () => {
|
|
|
253
254
|
await pressKey(setup, "RETURN", { meta: true });
|
|
254
255
|
// nothing pending despite the note, so the confirm card defaults to approve
|
|
255
256
|
await waitForText(setup, "[Approve]");
|
|
256
|
-
await
|
|
257
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
257
258
|
|
|
258
259
|
// Assert
|
|
259
260
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -273,7 +274,7 @@ describe("the guided walk", () => {
|
|
|
273
274
|
expect(foregroundsOf(setup, "new")).toContain(DARK.insertedForeground);
|
|
274
275
|
|
|
275
276
|
// Act
|
|
276
|
-
await
|
|
277
|
+
await navCommand(setup, "k");
|
|
277
278
|
|
|
278
279
|
// Assert
|
|
279
280
|
await waitForText(setup, "file 1 of 3 · 0 viewed");
|
|
@@ -289,7 +290,7 @@ describe("the guided walk", () => {
|
|
|
289
290
|
const setup = await renderApp();
|
|
290
291
|
|
|
291
292
|
// Act - renderApp already waited for the diff to load; the session is resolved
|
|
292
|
-
await
|
|
293
|
+
await navCommand(setup, "k");
|
|
293
294
|
|
|
294
295
|
// Assert
|
|
295
296
|
await waitForText(setup, "review submitted - read-only");
|
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
dragText,
|
|
22
22
|
isolateUserConfig,
|
|
23
23
|
locateText,
|
|
24
|
+
press,
|
|
24
25
|
pressKey,
|
|
25
26
|
typeText,
|
|
26
27
|
waitForState,
|
|
27
28
|
waitForText,
|
|
29
|
+
waitForTextGone,
|
|
28
30
|
renderReadyApp,
|
|
29
31
|
} from "./test-support";
|
|
30
32
|
import { NERD } from "./components/primitives/icons";
|
|
@@ -98,6 +100,7 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
98
100
|
|
|
99
101
|
/** The header controls sit on the top content row of the compact header. */
|
|
100
102
|
const HEADER_ROW = 0;
|
|
103
|
+
const BARE_NAV_HINT = "c comment · z fold · type to leave";
|
|
101
104
|
|
|
102
105
|
/** Column of a Project-header toggle, located by its own glyph so spacing changes never break the click. */
|
|
103
106
|
function toggleColumn(setup: Setup, glyph: string): number {
|
|
@@ -496,6 +499,89 @@ describe("the bare-launch welcome shell", () => {
|
|
|
496
499
|
expect(frame).toContain("edited in the working tree");
|
|
497
500
|
});
|
|
498
501
|
|
|
502
|
+
test("nav mode on the Welcome tab offers only comment and fold; a diff letter types", async () => {
|
|
503
|
+
const setup = await renderReadyApp(
|
|
504
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
505
|
+
{ width: 160, height: 28 },
|
|
506
|
+
);
|
|
507
|
+
await waitForText(setup, "quick brown fox");
|
|
508
|
+
const line = locateText(setup, "quick brown fox");
|
|
509
|
+
await setup.mockMouse.click(line.column, line.row);
|
|
510
|
+
|
|
511
|
+
// esc enters nav; the footer names what this surface can do, not the diff commands
|
|
512
|
+
await press(setup, "escape");
|
|
513
|
+
await waitForText(setup, BARE_NAV_HINT);
|
|
514
|
+
expect(setup.captureCharFrame()).not.toContain("x reject");
|
|
515
|
+
|
|
516
|
+
// a diff letter has no command here, so it leaves nav and types, as the footer says
|
|
517
|
+
await pressKey(setup, "d");
|
|
518
|
+
await waitForText(setup, "● d");
|
|
519
|
+
await press(setup, "escape");
|
|
520
|
+
await waitForTextGone(setup, "● d");
|
|
521
|
+
|
|
522
|
+
// c opens a comment on the caret word; z folds the saved card
|
|
523
|
+
await press(setup, "escape");
|
|
524
|
+
await waitForText(setup, BARE_NAV_HINT);
|
|
525
|
+
await pressKey(setup, "c");
|
|
526
|
+
await typeText(setup, "note");
|
|
527
|
+
await waitForText(setup, "● note");
|
|
528
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
529
|
+
await waitForText(setup, "note");
|
|
530
|
+
await press(setup, "escape");
|
|
531
|
+
await waitForText(setup, BARE_NAV_HINT);
|
|
532
|
+
await pressKey(setup, "z");
|
|
533
|
+
await waitForText(setup, "○ 1 comment ›");
|
|
534
|
+
|
|
535
|
+
setup.renderer.destroy();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("nav mode on a bare changed-file diff has no reject or walk; x types instead", async () => {
|
|
539
|
+
const setup = await renderReadyApp(
|
|
540
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
541
|
+
{ width: 160, height: 28 },
|
|
542
|
+
);
|
|
543
|
+
await waitForText(setup, "README.md");
|
|
544
|
+
const readme = locateText(setup, "README.md");
|
|
545
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
546
|
+
await waitForText(setup, "edited in the working tree");
|
|
547
|
+
const line = locateText(setup, "edited in the working tree");
|
|
548
|
+
await setup.mockMouse.click(line.column, line.row);
|
|
549
|
+
|
|
550
|
+
await press(setup, "escape");
|
|
551
|
+
await waitForText(setup, BARE_NAV_HINT);
|
|
552
|
+
expect(setup.captureCharFrame()).not.toContain("x reject");
|
|
553
|
+
await pressKey(setup, "x");
|
|
554
|
+
await waitForText(setup, "● x");
|
|
555
|
+
const frame = setup.captureCharFrame();
|
|
556
|
+
expect(frame).toContain("edited in the working tree");
|
|
557
|
+
expect(frame).not.toContain("change rejected");
|
|
558
|
+
|
|
559
|
+
setup.renderer.destroy();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("nav mode on a project file offers only comment and fold", async () => {
|
|
563
|
+
const setup = await renderReadyApp(
|
|
564
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
565
|
+
{ width: 160, height: 28 },
|
|
566
|
+
);
|
|
567
|
+
await waitForText(setup, "README.md");
|
|
568
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
569
|
+
await waitForText(setup, "README.md");
|
|
570
|
+
const readme = locateText(setup, "README.md");
|
|
571
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
572
|
+
await waitForText(setup, "edited in the working tree");
|
|
573
|
+
const line = locateText(setup, "edited in the working tree");
|
|
574
|
+
await setup.mockMouse.click(line.column, line.row);
|
|
575
|
+
|
|
576
|
+
await press(setup, "escape");
|
|
577
|
+
await waitForText(setup, BARE_NAV_HINT);
|
|
578
|
+
expect(setup.captureCharFrame()).not.toContain("x reject");
|
|
579
|
+
await pressKey(setup, "d");
|
|
580
|
+
await waitForText(setup, "● d");
|
|
581
|
+
|
|
582
|
+
setup.renderer.destroy();
|
|
583
|
+
});
|
|
584
|
+
|
|
499
585
|
test("a bare launch rides a Welcome tab in the editor while the Thread pane waits empty", async () => {
|
|
500
586
|
const setup = await renderWelcome();
|
|
501
587
|
|
|
@@ -11,6 +11,7 @@ import type { Theme } from "./theme";
|
|
|
11
11
|
export const UNDERLINE = createTextAttributes({ underline: true });
|
|
12
12
|
export const CUT = createTextAttributes({ strikethrough: true, dim: true });
|
|
13
13
|
export const BOLD = createTextAttributes({ bold: true });
|
|
14
|
+
export const ITALIC = createTextAttributes({ italic: true });
|
|
14
15
|
|
|
15
16
|
/** The marked-words treatment: a violet backdrop under an underline. */
|
|
16
17
|
const MARK_BACKDROP_DARK = "#463852";
|
|
@@ -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
|
@@ -371,12 +371,8 @@ export function NoThreadShell(props: {
|
|
|
371
371
|
{menuChrome}
|
|
372
372
|
<ConfirmDialog
|
|
373
373
|
isOpen={confirming !== null}
|
|
374
|
-
title="
|
|
375
|
-
message={
|
|
376
|
-
confirming
|
|
377
|
-
? `Delete "${confirming.title}"? This removes the plan and its review.`
|
|
378
|
-
: ""
|
|
379
|
-
}
|
|
374
|
+
title=" delete thread "
|
|
375
|
+
message={confirming ? `Delete "${confirming.title}"? This removes the thread.` : ""}
|
|
380
376
|
onConfirm={() => {
|
|
381
377
|
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
382
378
|
setMode({ type: "normal" });
|
|
@@ -388,7 +384,6 @@ export function NoThreadShell(props: {
|
|
|
388
384
|
<PromptDialog
|
|
389
385
|
isOpen
|
|
390
386
|
title=" rename thread "
|
|
391
|
-
label="new title for this thread:"
|
|
392
387
|
value={mode.text}
|
|
393
388
|
placeholder="a short title"
|
|
394
389
|
onInput={(text) => setMode({ ...mode, text })}
|
|
@@ -412,8 +407,11 @@ export function CompletionScreen(props: {
|
|
|
412
407
|
verdict: VerdictKind;
|
|
413
408
|
completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
|
|
414
409
|
status: string;
|
|
410
|
+
onClose: () => void;
|
|
411
|
+
onBackToPlan: () => void;
|
|
412
|
+
onAlways: () => void;
|
|
415
413
|
}): React.ReactNode {
|
|
416
|
-
const { theme, session, verdict, completion, status } = props;
|
|
414
|
+
const { theme, session, verdict, completion, status, onClose, onBackToPlan, onAlways } = props;
|
|
417
415
|
|
|
418
416
|
return (
|
|
419
417
|
<ThemeProvider theme={theme}>
|
|
@@ -421,6 +419,9 @@ export function CompletionScreen(props: {
|
|
|
421
419
|
verdict={verdict}
|
|
422
420
|
completion={completion}
|
|
423
421
|
status={status}
|
|
422
|
+
onClose={onClose}
|
|
423
|
+
onBackToPlan={onBackToPlan}
|
|
424
|
+
onAlways={onAlways}
|
|
424
425
|
returnsTo={
|
|
425
426
|
returnPaneFor(session.artifact.meta.herdrPane)
|
|
426
427
|
? (session.artifact.meta.agent ?? "the agent")
|
|
@@ -510,7 +511,6 @@ export function TrailingOverlays(props: {
|
|
|
510
511
|
<PromptDialog
|
|
511
512
|
isOpen
|
|
512
513
|
title=" rename thread "
|
|
513
|
-
label="new title for this thread:"
|
|
514
514
|
value={mode.text}
|
|
515
515
|
placeholder="a short title"
|
|
516
516
|
onInput={(text) => setMode({ ...mode, text })}
|
|
@@ -519,6 +519,16 @@ export function TrailingOverlays(props: {
|
|
|
519
519
|
theme={theme}
|
|
520
520
|
/>
|
|
521
521
|
) : null}
|
|
522
|
+
{mode.type === "confirmDelete" ? (
|
|
523
|
+
<ConfirmDialog
|
|
524
|
+
isOpen
|
|
525
|
+
title=" delete thread "
|
|
526
|
+
message={`Delete "${mode.title}"? This removes the thread.`}
|
|
527
|
+
onConfirm={() => dispatch({ type: "confirmDialog" })}
|
|
528
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
529
|
+
theme={theme}
|
|
530
|
+
/>
|
|
531
|
+
) : null}
|
|
522
532
|
{mode.type === "nameSelf" ? (
|
|
523
533
|
<PromptDialog
|
|
524
534
|
isOpen
|
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";
|
|
@@ -57,7 +58,6 @@ export function resolveOverlay(
|
|
|
57
58
|
mode.type === "treePrompt"
|
|
58
59
|
)
|
|
59
60
|
return "prompt";
|
|
60
|
-
if (mode.type === "shareChoice") return "shareChoice";
|
|
61
61
|
if (mode.type === "spanActions") return "spanActions";
|
|
62
62
|
if (completionPhase === "prompt") return "completion-prompt";
|
|
63
63
|
if (completionPhase === "counting") return "completion-counting";
|
|
@@ -136,6 +136,7 @@ export function buildSubmitConfirmState(
|
|
|
136
136
|
session: Thread;
|
|
137
137
|
walkFileList: WalkFile[];
|
|
138
138
|
viewedPaths: Set<string>;
|
|
139
|
+
quickActions: QuickAction[];
|
|
139
140
|
},
|
|
140
141
|
): Omit<ConfirmCardProps, "theme"> | null {
|
|
141
142
|
const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
|
|
@@ -145,6 +146,7 @@ export function buildSubmitConfirmState(
|
|
|
145
146
|
return {
|
|
146
147
|
verdict: mode.verdict,
|
|
147
148
|
summary: mode.summary,
|
|
149
|
+
quickActions: deps.quickActions,
|
|
148
150
|
viewedSummary:
|
|
149
151
|
isDiff && session.viewedPaths !== undefined
|
|
150
152
|
? `${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 = {
|