@cueloop/client 0.1.0-alpha.97 → 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.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +31 -8
- package/src/annotations/use-annotation-surface.tsx +257 -31
- 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.thread-agent.test.ts +12 -0
- package/src/app/App.tsx +223 -97
- package/src/app/App.visual.test.tsx +2 -0
- package/src/app/App.workbench.test.tsx +53 -0
- package/src/app/app-screens.tsx +3 -0
- package/src/app/components/AppShell.tsx +1 -0
- package/src/app/components/AppShell.zoom.test.tsx +4 -3
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/run.ts +1 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
- package/src/diff/components/AnnotatableFileView.tsx +8 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +10 -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 +62 -16
- 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/settings/config.test.ts +15 -0
- package/src/settings/config.ts +14 -4
- package/src/stories/__snapshots__/stories.test.tsx.snap +74 -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-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.test.tsx +2 -0
- package/src/thread/components/ThreadFooter.tsx +3 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +25 -5
- 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 +42 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- 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 +7 -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
|
@@ -36,7 +36,12 @@ import { wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
|
|
|
36
36
|
import { MarkdownGridBlock } from "./MarkdownGridBlock";
|
|
37
37
|
import { useFrameMeasure } from "../../ui/use-frame-measure";
|
|
38
38
|
import { scrollBoxDragViewport, useTerminalVirtualizer } from "../../ui/use-terminal-virtualizer";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
useAnnotationSurface,
|
|
41
|
+
type LineSource,
|
|
42
|
+
type PromptFocusRequest,
|
|
43
|
+
type PromptRestoreRequest,
|
|
44
|
+
} from "../../annotations/use-annotation-surface";
|
|
40
45
|
import { NavModeHint } from "../../keyboard/components/NavModeHint";
|
|
41
46
|
import { DiscussionMarkerRail } from "../../annotations/components/DiscussionMarkerRail";
|
|
42
47
|
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
@@ -150,6 +155,17 @@ export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
|
150
155
|
];
|
|
151
156
|
|
|
152
157
|
export interface ThreadViewProps {
|
|
158
|
+
/** Agent transcripts permit feedback only on finalized answer blocks. */
|
|
159
|
+
canAnnotateBlock?: (blockIndex: number) => boolean;
|
|
160
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
161
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
162
|
+
requestedBlock?: { blockIndex: number };
|
|
163
|
+
onInvoke?: () => void;
|
|
164
|
+
/** The final prompt line uses a plain composer instead of a comment card. */
|
|
165
|
+
isPromptBlock?: (blockIndex: number) => boolean;
|
|
166
|
+
promptFocusRequest?: PromptFocusRequest;
|
|
167
|
+
promptRestoreRequest?: PromptRestoreRequest;
|
|
168
|
+
renderBlock?: (index: number) => React.ReactNode | undefined;
|
|
153
169
|
session: Thread;
|
|
154
170
|
display: DisplayBlock[];
|
|
155
171
|
marks: Map<number, Mark[]>;
|
|
@@ -202,13 +218,22 @@ export function ThreadView({
|
|
|
202
218
|
onNavCommand,
|
|
203
219
|
onExit,
|
|
204
220
|
theme,
|
|
221
|
+
canAnnotateBlock,
|
|
222
|
+
isAnnotationReadOnly,
|
|
223
|
+
annotationAction,
|
|
224
|
+
requestedBlock,
|
|
225
|
+
onInvoke,
|
|
226
|
+
isPromptBlock,
|
|
227
|
+
promptFocusRequest,
|
|
228
|
+
promptRestoreRequest,
|
|
229
|
+
renderBlock,
|
|
205
230
|
}: ThreadViewProps): React.ReactNode {
|
|
206
231
|
const tokens = useComponentTheme(theme);
|
|
207
232
|
const source: LineSource = {
|
|
208
233
|
count: display.length,
|
|
209
234
|
// the surface hit-tests and paints in rendered text (inline markers concealed); a grid anchors on its raw source
|
|
210
235
|
textAt: (blockIndex) => renderedText(display[blockIndex]!),
|
|
211
|
-
annotatable: () => true,
|
|
236
|
+
annotatable: canAnnotateBlock ?? (() => true),
|
|
212
237
|
};
|
|
213
238
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
214
239
|
const surface = useAnnotationSurface({
|
|
@@ -228,6 +253,13 @@ export function ThreadView({
|
|
|
228
253
|
onAnnotate,
|
|
229
254
|
onReply,
|
|
230
255
|
onUpdateAnnotation,
|
|
256
|
+
isAnnotationReadOnly,
|
|
257
|
+
annotationAction,
|
|
258
|
+
requestedBlock,
|
|
259
|
+
onInvoke,
|
|
260
|
+
isPromptBlock,
|
|
261
|
+
promptFocusRequest,
|
|
262
|
+
promptRestoreRequest,
|
|
231
263
|
dragViewport: () => scrollBoxDragViewport(scrollRef.current),
|
|
232
264
|
resolveAuthorLabel,
|
|
233
265
|
onNavCommand,
|
|
@@ -316,7 +348,7 @@ export function ThreadView({
|
|
|
316
348
|
</span>
|
|
317
349
|
);
|
|
318
350
|
|
|
319
|
-
const lineRowFor = (context: LineContext): React.ReactNode => {
|
|
351
|
+
const lineRowFor = (context: LineContext, hasComposer = false): React.ReactNode => {
|
|
320
352
|
const {
|
|
321
353
|
blockIndex,
|
|
322
354
|
roleRuns,
|
|
@@ -329,6 +361,9 @@ export function ThreadView({
|
|
|
329
361
|
syntaxSpans,
|
|
330
362
|
} = context;
|
|
331
363
|
|
|
364
|
+
// The prompt composer occupies its blank line instead of adding a row below it.
|
|
365
|
+
if (isPromptBlock?.(blockIndex) && hasComposer) return null;
|
|
366
|
+
|
|
332
367
|
return (
|
|
333
368
|
<box key={`line-${lineIndex}`} style={{ flexDirection: "row" }}>
|
|
334
369
|
<text selectable={false}>
|
|
@@ -366,6 +401,14 @@ export function ThreadView({
|
|
|
366
401
|
};
|
|
367
402
|
|
|
368
403
|
const blockNodeFor = (blockIndex: number): React.ReactNode => {
|
|
404
|
+
const custom = renderBlock?.(blockIndex);
|
|
405
|
+
|
|
406
|
+
if (custom !== undefined)
|
|
407
|
+
return (
|
|
408
|
+
<box key={`custom-${blockIndex}`} ref={virtual.measureRef(blockIndex)}>
|
|
409
|
+
{custom}
|
|
410
|
+
</box>
|
|
411
|
+
);
|
|
369
412
|
const block = display[blockIndex]!;
|
|
370
413
|
const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
|
|
371
414
|
// list items of one list stay tight; every other block sits a blank row below its neighbour
|
|
@@ -422,22 +465,25 @@ export function ThreadView({
|
|
|
422
465
|
const line = lines[lineIndex]!;
|
|
423
466
|
const isLastLine = lineIndex === lines.length - 1;
|
|
424
467
|
|
|
468
|
+
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
469
|
+
|
|
425
470
|
lineRows.push(
|
|
426
|
-
lineRowFor(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
471
|
+
lineRowFor(
|
|
472
|
+
{
|
|
473
|
+
blockIndex,
|
|
474
|
+
roleRuns,
|
|
475
|
+
line,
|
|
476
|
+
lineIndex,
|
|
477
|
+
ranges,
|
|
478
|
+
marker,
|
|
479
|
+
baseFg,
|
|
480
|
+
baseAttributes,
|
|
481
|
+
syntaxSpans,
|
|
482
|
+
},
|
|
483
|
+
cards.length > 0,
|
|
484
|
+
),
|
|
437
485
|
);
|
|
438
486
|
if (isLastLine) lineRows.push(headingRule(block));
|
|
439
|
-
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
440
|
-
|
|
441
487
|
lineRows.push(...cards);
|
|
442
488
|
if (cards.length > 0 && !isLastLine) {
|
|
443
489
|
lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
|
|
@@ -38,6 +38,7 @@ export function runsFor(text: string, ranges: MarkRange[]): Run[] {
|
|
|
38
38
|
|
|
39
39
|
if (end <= start) continue;
|
|
40
40
|
const covering = ranges.filter((range) => range.start <= start && end <= range.end);
|
|
41
|
+
|
|
41
42
|
// the caret cell shows through a mark, so the head of a selection is visible
|
|
42
43
|
runs.push({
|
|
43
44
|
text: text.slice(start, end),
|
|
@@ -25,6 +25,7 @@ export interface MarkdownHighlightRange {
|
|
|
25
25
|
/** The absolute source offset where each 0-based line begins. */
|
|
26
26
|
function lineStartOffsets(source: string): number[] {
|
|
27
27
|
const offsets = [0];
|
|
28
|
+
|
|
28
29
|
for (let index = 0; index < source.length; index++) {
|
|
29
30
|
if (source[index] === "\n") offsets.push(index + 1);
|
|
30
31
|
}
|
|
@@ -39,6 +40,7 @@ const INLINE_TOKEN = /(`+)(?:.+?)\1|\[[^\]]+\]\([^)]+\)/g;
|
|
|
39
40
|
function scanInlineTokens(text: string, base: number, into: MarkdownHighlightRange[]): void {
|
|
40
41
|
INLINE_TOKEN.lastIndex = 0;
|
|
41
42
|
let match: RegExpExecArray | null;
|
|
43
|
+
|
|
42
44
|
while ((match = INLINE_TOKEN.exec(text)) !== null) {
|
|
43
45
|
const token = match[0];
|
|
44
46
|
const start = base + match.index;
|
|
@@ -64,6 +64,7 @@ test("phaseMarksToSpans builds a scope root with one child span per phase under
|
|
|
64
64
|
|
|
65
65
|
expect(spans).toHaveLength(3);
|
|
66
66
|
const [root, first, second] = spans;
|
|
67
|
+
|
|
67
68
|
expect(root!.name).toBe("cueloop.startup");
|
|
68
69
|
expect(root!.parentSpanId).toBeUndefined();
|
|
69
70
|
expect(first!.name).toBe("cueloop.startup.renderer");
|
|
@@ -82,6 +83,7 @@ test("postOtlpTrace sends no request when the endpoint is unset", async () => {
|
|
|
82
83
|
return new Response("{}");
|
|
83
84
|
},
|
|
84
85
|
});
|
|
86
|
+
|
|
85
87
|
try {
|
|
86
88
|
await postOtlpTrace("cueloop-client", [span]);
|
|
87
89
|
} finally {
|
|
@@ -101,6 +103,7 @@ test("postOtlpTrace POSTs OTLP JSON to /v1/traces when the endpoint is set", asy
|
|
|
101
103
|
return new Response("{}");
|
|
102
104
|
},
|
|
103
105
|
});
|
|
106
|
+
|
|
104
107
|
// a trailing slash must not double up into "//v1/traces"
|
|
105
108
|
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = `http://127.0.0.1:${server.port}/`;
|
|
106
109
|
try {
|
|
@@ -28,6 +28,7 @@ export interface TraceSpan {
|
|
|
28
28
|
|
|
29
29
|
function randomHex(bytes: number): string {
|
|
30
30
|
const buffer = new Uint8Array(bytes);
|
|
31
|
+
|
|
31
32
|
crypto.getRandomValues(buffer);
|
|
32
33
|
|
|
33
34
|
return Array.from(buffer, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -97,6 +98,7 @@ export function phaseMarksToSpans(scope: string, marks: readonly PhaseMark[]): T
|
|
|
97
98
|
let cursorMs = startMs;
|
|
98
99
|
const children = marks.map((mark) => {
|
|
99
100
|
const phaseStartMs = cursorMs;
|
|
101
|
+
|
|
100
102
|
cursorMs += mark.elapsedMs;
|
|
101
103
|
|
|
102
104
|
return {
|
|
@@ -118,10 +120,12 @@ export async function postOtlpTrace(
|
|
|
118
120
|
spans: readonly TraceSpan[],
|
|
119
121
|
): Promise<void> {
|
|
120
122
|
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
123
|
+
|
|
121
124
|
if (endpoint === undefined || spans.length === 0) return;
|
|
122
125
|
|
|
123
126
|
// strip a trailing slash so an endpoint like "http://host:4318/" does not become "//v1/traces"
|
|
124
127
|
const base = endpoint.replace(/\/+$/, "");
|
|
128
|
+
|
|
125
129
|
await fetch(`${base}/v1/traces`, {
|
|
126
130
|
method: "POST",
|
|
127
131
|
headers: { "content-type": "application/json" },
|
|
@@ -10,6 +10,7 @@ const PERF_SERVICE_NAME = "cueloop-client";
|
|
|
10
10
|
|
|
11
11
|
export function reportPerfMarks(scope: string): void {
|
|
12
12
|
const marks = takePerfMarks();
|
|
13
|
+
|
|
13
14
|
if (marks.length === 0) return;
|
|
14
15
|
|
|
15
16
|
if (process.env.CUELOOP_PERF === "1") process.stderr.write(formatPerfBlock(scope, marks));
|
|
@@ -18,6 +18,7 @@ function perfInstrumentationEnabled(): boolean {
|
|
|
18
18
|
export function perfMark(label: string): void {
|
|
19
19
|
if (!perfInstrumentationEnabled()) return;
|
|
20
20
|
const now = performance.now();
|
|
21
|
+
|
|
21
22
|
phaseMarks.push({ label, elapsedMs: now - (lastMarkAt ?? 0) });
|
|
22
23
|
lastMarkAt = now;
|
|
23
24
|
}
|
|
@@ -99,12 +99,15 @@ describe("editing an expanded action", () => {
|
|
|
99
99
|
/>,
|
|
100
100
|
{ width: 60, height: 8 },
|
|
101
101
|
);
|
|
102
|
+
|
|
102
103
|
await settle(setup);
|
|
103
104
|
|
|
104
105
|
// clicking the title opens the title field; clicking the description opens the metadata field
|
|
105
106
|
const title = locateText(setup, "Ship it");
|
|
107
|
+
|
|
106
108
|
await setup.mockMouse.click(title.column, title.row);
|
|
107
109
|
const description = locateText(setup, "be terse");
|
|
110
|
+
|
|
108
111
|
await setup.mockMouse.click(description.column, description.row);
|
|
109
112
|
|
|
110
113
|
expect(toggles).toEqual([
|
|
@@ -659,3 +659,18 @@ describe("the /lgtm default quick action", () => {
|
|
|
659
659
|
expect(quickActionBody(lgtm!)).toBe("LGTM\n\nThis looks good to me.");
|
|
660
660
|
});
|
|
661
661
|
});
|
|
662
|
+
|
|
663
|
+
test("Thread agents default off and require a boolean experimental TOML flag", () => {
|
|
664
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-agent-config-"));
|
|
665
|
+
const path = join(dir, "config.toml");
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
|
|
669
|
+
writeFileSync(path, "[experimental]\nthread_agent = true\n");
|
|
670
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(true);
|
|
671
|
+
writeFileSync(path, '[experimental]\nthread_agent = "true"\n');
|
|
672
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
|
|
673
|
+
} finally {
|
|
674
|
+
rmSync(dir, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
});
|
package/src/settings/config.ts
CHANGED
|
@@ -144,6 +144,8 @@ export interface CueloopConfig {
|
|
|
144
144
|
export interface ExperimentalConfig {
|
|
145
145
|
/** Render a prototype as a pixel mockup (kitty graphics) instead of the default markdown design doc. */
|
|
146
146
|
prototypePixels: boolean;
|
|
147
|
+
/** Allow the local session owner to invoke a harness inside the Thread. */
|
|
148
|
+
threadAgent: boolean;
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/** Where a display name came from: typed by the user, or verified from a GitHub login. */
|
|
@@ -251,6 +253,7 @@ const ObsidianSchema = v.object({
|
|
|
251
253
|
const IntegrationsSchema = v.object({ obsidian: v.optional(ObsidianSchema) });
|
|
252
254
|
const ExperimentalSchema = v.object({
|
|
253
255
|
prototype_pixels: v.fallback(v.optional(v.boolean()), undefined),
|
|
256
|
+
thread_agent: v.fallback(v.optional(v.boolean()), undefined),
|
|
254
257
|
});
|
|
255
258
|
const ReviewSchema = v.object({
|
|
256
259
|
skill: v.fallback(v.optional(v.string()), undefined),
|
|
@@ -369,9 +372,7 @@ function layer(
|
|
|
369
372
|
if (integrations.success && integrations.output.obsidian) {
|
|
370
373
|
mergeObsidian(out.integrations.obsidian, integrations.output.obsidian);
|
|
371
374
|
}
|
|
372
|
-
if (experimental.success
|
|
373
|
-
out.experimental.prototypePixels = experimental.output.prototype_pixels;
|
|
374
|
-
}
|
|
375
|
+
if (experimental.success) applyExperimental(out.experimental, experimental.output);
|
|
375
376
|
if (review.success) applyReview(out.review, review.output);
|
|
376
377
|
|
|
377
378
|
return out;
|
|
@@ -397,7 +398,7 @@ export function loadConfig(
|
|
|
397
398
|
skillsPath: join(homedir(), ".agents", "skills"),
|
|
398
399
|
review: { skill: "code-review", workspace: "worktree" },
|
|
399
400
|
integrations: { obsidian: { ...OBSIDIAN_DEFAULTS } },
|
|
400
|
-
experimental: { prototypePixels: false },
|
|
401
|
+
experimental: { prototypePixels: false, threadAgent: false },
|
|
401
402
|
};
|
|
402
403
|
// Theme name and per-token overrides are separate concerns, composed once
|
|
403
404
|
// after all layers: the last file to set [ui] theme wins, and every [theme]
|
|
@@ -629,3 +630,12 @@ export function persistAuthorName(id: string, name: string, userConfigPath?: str
|
|
|
629
630
|
mkdirSync(dirname(path), { recursive: true });
|
|
630
631
|
writeFileSync(path, text);
|
|
631
632
|
}
|
|
633
|
+
|
|
634
|
+
/** Only explicitly configured experimental switches override the preceding layer. */
|
|
635
|
+
function applyExperimental(
|
|
636
|
+
config: ExperimentalConfig,
|
|
637
|
+
raw: v.InferOutput<typeof ExperimentalSchema>,
|
|
638
|
+
): void {
|
|
639
|
+
if (raw.prototype_pixels !== undefined) config.prototypePixels = raw.prototype_pixels;
|
|
640
|
+
if (raw.thread_agent !== undefined) config.threadAgent = raw.thread_agent;
|
|
641
|
+
}
|
|
@@ -2282,3 +2282,77 @@ exports[`stories catalog Collaborator Join/JoinSplash renders, snapshots, and ca
|
|
|
2282
2282
|
|
|
2283
2283
|
"
|
|
2284
2284
|
`;
|
|
2285
|
+
|
|
2286
|
+
exports["stories catalog Surfaces/AgentThreadPane/CompletedAnswer renders, snapshots, and carries its colors: Surfaces/AgentThreadPane/CompletedAnswer 1"] = "\n\" Artifact Agent fx ready Tools (1) \nWorkspace: /project \n \n You \n ────────────────────────────────────────────────────────────────────── \n \n Why does the retry survive cancellation? \n \n 1 tool call · 1 reads · 0 commands \n \n fx \n ────────────────────────────────────────────────────────────────────── \n \n The timer survives cancellation. Clear it when the request ends. \n \n \n \n \n \n \n \n \n \n \n────────────────────────────────────────────────────────────────────────────────\n Ask fx… Ctrl+Enter sends · Esc reads · i composes \n \n Send message (0) \n\"\n";
|
|
2287
|
+
|
|
2288
|
+
exports["stories catalog Surfaces/AgentThreadPane/WaitingPermission renders, snapshots, and carries its colors: Surfaces/AgentThreadPane/WaitingPermission 1"] = "\n\" Artifact Agent fx working Tools (1) Stop \nWorkspace: /project \n \n You \n ────────────────────────────────────────────────────────────────────── \n \n Why does the retry survive cancellation? \n \n 1 tool call · 1 reads · 0 commands \n \n fx \n ────────────────────────────────────────────────────────────────────── \n \n The timer survives cancellation. Clear it when the request ends. \n \n \n \n \n \n \n┌──────────────────────────────────────────────────────────────────────────────┐\n│ Update retry.ts │\n│ Allow once Reject once │\n└──────────────────────────────────────────────────────────────────────────────┘\n────────────────────────────────────────────────────────────────────────────────\n Wait for fx, or stop the tur \n \n Send message (0) \n\"\n";
|
|
2289
|
+
|
|
2290
|
+
exports[`stories catalog Chrome/AgentConfigControls/Choices renders, snapshots, and carries its colors: Chrome/AgentConfigControls/Choices 1`] = `
|
|
2291
|
+
"Test model Medium
|
|
2292
|
+
|
|
2293
|
+
"
|
|
2294
|
+
`;
|
|
2295
|
+
|
|
2296
|
+
exports[`stories catalog Surfaces/AgentThreadPane/CompletedAnswer renders, snapshots, and carries its colors: Surfaces/AgentThreadPane/CompletedAnswer 1`] = `
|
|
2297
|
+
"
|
|
2298
|
+
Keep the retry bounded.
|
|
2299
|
+
|
|
2300
|
+
Why does the retry survive cancellation?
|
|
2301
|
+
|
|
2302
|
+
The timer survives cancellation. Clear it when the request ends.
|
|
2303
|
+
|
|
2304
|
+
|
|
2305
|
+
|
|
2306
|
+
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
|
|
2318
|
+
|
|
2319
|
+
|
|
2320
|
+
|
|
2321
|
+
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
type to comment · esc for nav mode
|
|
2325
|
+
"
|
|
2326
|
+
`;
|
|
2327
|
+
|
|
2328
|
+
exports[`stories catalog Surfaces/AgentThreadPane/WaitingPermission renders, snapshots, and carries its colors: Surfaces/AgentThreadPane/WaitingPermission 1`] = `
|
|
2329
|
+
"
|
|
2330
|
+
Keep the retry bounded.
|
|
2331
|
+
|
|
2332
|
+
Why does the retry survive cancellation?
|
|
2333
|
+
|
|
2334
|
+
The timer survives cancellation. Clear it when the request ends.
|
|
2335
|
+
Thinking…
|
|
2336
|
+
Update retry.ts
|
|
2337
|
+
Allow once Reject once
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
|
|
2356
|
+
type to comment · esc for nav mode
|
|
2357
|
+
"
|
|
2358
|
+
`;
|
|
@@ -59,6 +59,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
|
59
59
|
const toggleFolder = (id: string): void => {
|
|
60
60
|
setExpandedIds((current) => {
|
|
61
61
|
const next = new Set(current);
|
|
62
|
+
|
|
62
63
|
if (next.has(id)) next.delete(id);
|
|
63
64
|
else next.add(id);
|
|
64
65
|
|
|
@@ -73,6 +74,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
|
73
74
|
const collapseFolder = (id: string): void => {
|
|
74
75
|
setExpandedIds((current) => {
|
|
75
76
|
const next = new Set(current);
|
|
77
|
+
|
|
76
78
|
next.delete(id);
|
|
77
79
|
|
|
78
80
|
return next;
|
|
@@ -83,6 +85,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
|
83
85
|
const index = rows.findIndex((row) => row.id === selectedId);
|
|
84
86
|
const nextIndex = Math.max(0, Math.min(rows.length - 1, index + delta));
|
|
85
87
|
const nextRow = rows[nextIndex];
|
|
88
|
+
|
|
86
89
|
if (nextRow !== undefined) setSelectedId(nextRow.id);
|
|
87
90
|
};
|
|
88
91
|
|
package/src/stories/story.ts
CHANGED
|
@@ -97,6 +97,7 @@ function hoistSingleStoryFolders(nodes: TreeNode[]): TreeNode[] {
|
|
|
97
97
|
if (node.children === undefined) return node;
|
|
98
98
|
const children = hoistSingleStoryFolders(node.children);
|
|
99
99
|
const onlyChild = children.length === 1 ? children[0] : undefined;
|
|
100
|
+
|
|
100
101
|
if (
|
|
101
102
|
onlyChild !== undefined &&
|
|
102
103
|
onlyChild.children === undefined &&
|
|
@@ -126,6 +127,7 @@ export function buildStoryTree(stories: LoadedStory[]): TreeNode[] {
|
|
|
126
127
|
for (const segment of [section, ...moduleTitle.split("/")]) {
|
|
127
128
|
path = path === "" ? segment : `${path}/${segment}`;
|
|
128
129
|
let folder = foldersByPath.get(path);
|
|
130
|
+
|
|
129
131
|
if (folder === undefined) {
|
|
130
132
|
folder = { id: path, label: segment, children: [] };
|
|
131
133
|
foldersByPath.set(path, folder);
|
package/src/terminal/pty.ts
CHANGED
|
@@ -228,6 +228,7 @@ class Pty implements IPty {
|
|
|
228
228
|
this.exitEvent.fire({ exitCode });
|
|
229
229
|
} else {
|
|
230
230
|
const delay = Math.min(READ_IDLE_MAX_MS, READ_IDLE_MS * 2 ** Math.min(idlePolls, 5));
|
|
231
|
+
|
|
231
232
|
idlePolls += 1;
|
|
232
233
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
233
234
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { agentContinuation, type AgentContinuation } from "./agent-continuation";
|
|
3
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
4
|
+
|
|
5
|
+
const state: ThreadAgentState = {
|
|
6
|
+
threadId: "thread",
|
|
7
|
+
phase: { kind: "idle" },
|
|
8
|
+
messages: [],
|
|
9
|
+
comments: [],
|
|
10
|
+
tools: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
test("only a completed reply in an idle conversation offers continuation", () => {
|
|
14
|
+
expect(agentContinuation(state)).toEqual({ kind: "waiting" });
|
|
15
|
+
const message = {
|
|
16
|
+
id: "answer",
|
|
17
|
+
role: "agent" as const,
|
|
18
|
+
text: "Done",
|
|
19
|
+
complete: true,
|
|
20
|
+
revision: 1,
|
|
21
|
+
};
|
|
22
|
+
const completed: AgentContinuation = agentContinuation({ ...state, messages: [message] });
|
|
23
|
+
|
|
24
|
+
expect(completed.kind).toBe("ready");
|
|
25
|
+
if (completed.kind === "ready") expect(completed.replyId).toBe("answer");
|
|
26
|
+
expect(agentContinuation({ ...state, phase: { kind: "running" }, messages: [message] })).toEqual({
|
|
27
|
+
kind: "waiting",
|
|
28
|
+
});
|
|
29
|
+
expect(agentContinuation({ ...state, messages: [{ ...message, complete: false }] })).toEqual({
|
|
30
|
+
kind: "waiting",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
2
|
+
|
|
3
|
+
/** Chat continuation names a completed reply only when the harness has returned to idle. */
|
|
4
|
+
export type AgentContinuation = { kind: "waiting" } | { kind: "ready"; replyId: string };
|
|
5
|
+
|
|
6
|
+
/** Streaming replies cannot request the next prompt's focus. */
|
|
7
|
+
export function agentContinuation(state: ThreadAgentState): AgentContinuation {
|
|
8
|
+
const reply = state.messages.findLast((message) => message.role === "agent");
|
|
9
|
+
|
|
10
|
+
if (state.phase.kind !== "idle" || !reply?.complete) return { kind: "waiting" };
|
|
11
|
+
|
|
12
|
+
return { kind: "ready", replyId: reply.id };
|
|
13
|
+
}
|