@oh-my-pi/pi-coding-agent 15.10.2 → 15.10.4
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/CHANGELOG.md +66 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +1 -1
- package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
- package/dist/types/eval/idle-timeout.d.ts +1 -1
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +5 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/commit/agentic/agent.ts +1 -0
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/agent-bridge.test.ts +13 -0
- package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/agent-bridge.ts +6 -1
- package/src/eval/bridge-timeout.ts +1 -1
- package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
- package/src/eval/idle-timeout.ts +1 -1
- package/src/eval/js/context-manager.ts +66 -6
- package/src/eval/js/shared/prelude.txt +28 -12
- package/src/eval/js/tool-bridge.ts +3 -3
- package/src/eval/js/worker-entry.ts +6 -0
- package/src/eval/py/prelude.py +3 -3
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/index.ts +128 -52
- package/src/main.ts +54 -14
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/controllers/event-controller.ts +6 -1
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system.md +16 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/sdk.ts +59 -1
- package/src/session/agent-session.ts +5 -3
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +2 -2
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +1 -0
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +0 -7
- package/src/tools/eval-render.ts +6 -25
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +148 -106
- package/src/tools/index.ts +32 -0
- package/src/tools/path-utils.ts +19 -22
- package/src/tools/read.ts +16 -8
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +3 -12
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/title-generator.ts +2 -2
- /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Component,
|
|
3
3
|
Container,
|
|
4
|
-
|
|
4
|
+
fuzzyMatch,
|
|
5
5
|
Input,
|
|
6
6
|
matchesKey,
|
|
7
7
|
padding,
|
|
@@ -46,43 +46,107 @@ function formatSessionStatus(status: SessionStatus | undefined): string | undefi
|
|
|
46
46
|
/** Returns the IDs of sessions whose recorded prompts match a query, best first. */
|
|
47
47
|
export type SessionHistoryMatcher = (query: string) => string[];
|
|
48
48
|
|
|
49
|
+
function sessionSearchText(session: SessionInfo): string {
|
|
50
|
+
const parts = [
|
|
51
|
+
session.id,
|
|
52
|
+
session.title ?? "",
|
|
53
|
+
session.cwd ?? "",
|
|
54
|
+
session.firstMessage ?? "",
|
|
55
|
+
session.allMessagesText,
|
|
56
|
+
session.path,
|
|
57
|
+
];
|
|
58
|
+
return parts.filter(Boolean).join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function tokenizeSessionQuery(query: string): string[] {
|
|
62
|
+
const trimmed = query.trim().toLowerCase();
|
|
63
|
+
return trimmed ? trimmed.split(/\s+/) : [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function compareSessionRecency(a: SessionInfo, b: SessionInfo): number {
|
|
67
|
+
return b.modified.getTime() - a.modified.getTime();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Filter and rank session picker search results.
|
|
72
|
+
*
|
|
73
|
+
* Resume search narrows a recency-sorted list: once every query token appears
|
|
74
|
+
* as a literal substring, newer sessions should beat a slightly better fuzzy
|
|
75
|
+
* position match. Pure fuzzy/acronym matches still sort by fuzzy score after
|
|
76
|
+
* literal matches.
|
|
77
|
+
*/
|
|
78
|
+
export function rankSessionSearchMatches(allSessions: SessionInfo[], query: string): SessionInfo[] {
|
|
79
|
+
const tokens = tokenizeSessionQuery(query);
|
|
80
|
+
if (tokens.length === 0) return allSessions;
|
|
81
|
+
|
|
82
|
+
const results: Array<{ session: SessionInfo; score: number; literal: boolean; index: number }> = [];
|
|
83
|
+
for (let index = 0; index < allSessions.length; index++) {
|
|
84
|
+
const session = allSessions[index]!;
|
|
85
|
+
const text = sessionSearchText(session);
|
|
86
|
+
const textLower = text.toLowerCase();
|
|
87
|
+
let score = 0;
|
|
88
|
+
let literal = true;
|
|
89
|
+
let matches = true;
|
|
90
|
+
|
|
91
|
+
for (const token of tokens) {
|
|
92
|
+
const match = fuzzyMatch(token, textLower);
|
|
93
|
+
if (!match.matches) {
|
|
94
|
+
matches = false;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
score += match.score;
|
|
98
|
+
if (!textLower.includes(token)) literal = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (matches) results.push({ session, score, literal, index });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
results.sort((a, b) => {
|
|
105
|
+
if (a.literal !== b.literal) return a.literal ? -1 : 1;
|
|
106
|
+
if (a.literal) return compareSessionRecency(a.session, b.session) || a.index - b.index;
|
|
107
|
+
return a.score - b.score || compareSessionRecency(a.session, b.session) || a.index - b.index;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return results.map(result => result.session);
|
|
111
|
+
}
|
|
112
|
+
|
|
49
113
|
/**
|
|
50
|
-
* Combine
|
|
51
|
-
*
|
|
114
|
+
* Combine metadata matches with prompt-history matches for ranking, using both
|
|
115
|
+
* signals rather than replacing one with the other.
|
|
52
116
|
*
|
|
53
|
-
* - `fuzzy` is the ordered
|
|
117
|
+
* - `fuzzy` is the ordered metadata/session-text result.
|
|
54
118
|
* - `historyIds` are session IDs whose recorded prompts matched the query,
|
|
55
119
|
* ordered by prompt-history rank (typically newest matching prompt first); duplicates are tolerated.
|
|
56
120
|
*
|
|
57
|
-
* Ranking:
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
121
|
+
* Ranking: prompt-history matches lead in history order, then remaining
|
|
122
|
+
* metadata matches keep their existing order. A metadata match is never dropped,
|
|
123
|
+
* and history matches not present in `allSessions` (e.g. deleted or out-of-scope
|
|
124
|
+
* sessions) are ignored since they cannot be resumed from here.
|
|
61
125
|
*/
|
|
62
126
|
export function mergeSessionRanking(
|
|
63
127
|
allSessions: SessionInfo[],
|
|
64
128
|
fuzzy: SessionInfo[],
|
|
65
129
|
historyIds: string[],
|
|
66
130
|
): SessionInfo[] {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const both: SessionInfo[] = [];
|
|
74
|
-
const fuzzyOnly: SessionInfo[] = [];
|
|
75
|
-
const fuzzyPaths = new Set<string>();
|
|
76
|
-
for (const session of fuzzy) {
|
|
77
|
-
fuzzyPaths.add(session.path);
|
|
78
|
-
(historyRank.has(session.id) ? both : fuzzyOnly).push(session);
|
|
131
|
+
if (historyIds.length === 0) return fuzzy;
|
|
132
|
+
|
|
133
|
+
const sessionsById = new Map<string, SessionInfo>();
|
|
134
|
+
for (const session of allSessions) {
|
|
135
|
+
if (!sessionsById.has(session.id)) sessionsById.set(session.id, session);
|
|
79
136
|
}
|
|
80
137
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
const historyMatches: SessionInfo[] = [];
|
|
139
|
+
const historyPaths = new Set<string>();
|
|
140
|
+
for (const id of historyIds) {
|
|
141
|
+
const session = sessionsById.get(id);
|
|
142
|
+
if (!session || historyPaths.has(session.path)) continue;
|
|
143
|
+
historyMatches.push(session);
|
|
144
|
+
historyPaths.add(session.path);
|
|
145
|
+
}
|
|
146
|
+
if (historyMatches.length === 0) return fuzzy;
|
|
84
147
|
|
|
85
|
-
|
|
148
|
+
const metadataOnly = fuzzy.filter(session => !historyPaths.has(session.path));
|
|
149
|
+
return [...historyMatches, ...metadataOnly];
|
|
86
150
|
}
|
|
87
151
|
|
|
88
152
|
/**
|
|
@@ -156,17 +220,7 @@ class SessionList implements Component {
|
|
|
156
220
|
}
|
|
157
221
|
|
|
158
222
|
#filterSessions(query: string): void {
|
|
159
|
-
const fuzzy =
|
|
160
|
-
const parts = [
|
|
161
|
-
session.id,
|
|
162
|
-
session.title ?? "",
|
|
163
|
-
session.cwd ?? "",
|
|
164
|
-
session.firstMessage ?? "",
|
|
165
|
-
session.allMessagesText,
|
|
166
|
-
session.path,
|
|
167
|
-
];
|
|
168
|
-
return parts.filter(Boolean).join(" ");
|
|
169
|
-
});
|
|
223
|
+
const fuzzy = rankSessionSearchMatches(this.#allSessions, query);
|
|
170
224
|
this.#filteredSessions = this.#mergeHistoryMatches(query, fuzzy);
|
|
171
225
|
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, this.#filteredSessions.length - 1));
|
|
172
226
|
}
|
|
@@ -4,7 +4,7 @@ Use /tan to fork the current conversation into a background agent
|
|
|
4
4
|
Ctrl+D can be used to exit, but with your draft saved!
|
|
5
5
|
Find out which model you emotionally abuse the most with `omp stats`
|
|
6
6
|
Try task isolation to create CoW worktrees
|
|
7
|
-
|
|
7
|
+
Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask clanker to use it!
|
|
8
8
|
Spaghetti code? Try complaining with /omfg
|
|
9
9
|
Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
|
|
10
10
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
} from "@oh-my-pi/pi-tui";
|
|
16
16
|
import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
|
|
18
|
-
import { shimmerEnabled } from "../../modes/theme/shimmer";
|
|
19
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
20
19
|
import { theme } from "../../modes/theme/theme";
|
|
21
20
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
@@ -31,7 +30,7 @@ import {
|
|
|
31
30
|
renderJsonTreeLines,
|
|
32
31
|
} from "../../tools/json-tree";
|
|
33
32
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
34
|
-
import {
|
|
33
|
+
import { toolRenderers } from "../../tools/renderers";
|
|
35
34
|
import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
|
|
36
35
|
import { isFramedBlockComponent, renderStatusLine } from "../../tui";
|
|
37
36
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
@@ -133,9 +132,10 @@ export interface ToolExecutionHandle {
|
|
|
133
132
|
setExpanded(expanded: boolean): void;
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
/** Drive pending-tool redraws at 30fps so the
|
|
137
|
-
* smooth without spending twice the frame budget. The TUI
|
|
138
|
-
* cadence, and static frames diff to a no-op redraw at
|
|
135
|
+
/** Drive pending-tool redraws at 30fps so the running `task` row's shimmered
|
|
136
|
+
* subagent name stays smooth without spending twice the frame budget. The TUI
|
|
137
|
+
* throttles at the same cadence, and static frames diff to a no-op redraw at
|
|
138
|
+
* ~zero cost. */
|
|
139
139
|
const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
|
|
140
140
|
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
|
141
141
|
* render cadence (mirrors `Loader`). */
|
|
@@ -425,16 +425,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
425
425
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
426
426
|
const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
|
|
427
427
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
428
|
-
|
|
429
|
-
// not once they've been backgrounded: a backgrounded job's block gets
|
|
430
|
-
// committed to scrollback and finalizes later via the async update path, so a
|
|
431
|
-
// mid-sweep frame would freeze a stray dark "bar" segment into the border.
|
|
432
|
-
const isPendingExecBlock =
|
|
433
|
-
this.#isPartial &&
|
|
434
|
-
shimmerEnabled() &&
|
|
435
|
-
(this.#toolName === "bash" || this.#toolName === "eval") &&
|
|
436
|
-
!isBackgroundAsyncRunning;
|
|
437
|
-
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
428
|
+
const needsSpinner = isStreamingArgs || isPartialTask;
|
|
438
429
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
439
430
|
const now = performance.now();
|
|
440
431
|
const frameCount = theme.spinnerFrames.length;
|
|
@@ -446,7 +437,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
446
437
|
this.#spinnerInterval = setInterval(() => {
|
|
447
438
|
const now = performance.now();
|
|
448
439
|
const frameCount = theme.spinnerFrames.length;
|
|
449
|
-
// Redraw at 30fps for a smooth
|
|
440
|
+
// Redraw at 30fps for a smooth `task` name shimmer, but keep the spinner
|
|
450
441
|
// glyph phase-locked to its classic ~12.5fps cadence. Advancing the
|
|
451
442
|
// anchor by elapsed frames instead of resetting to `now` avoids the
|
|
452
443
|
// 30fps timer quantizing the glyph down to one step every three ticks.
|
|
@@ -529,39 +520,6 @@ export class ToolExecutionComponent extends Container {
|
|
|
529
520
|
return (this.#result.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
530
521
|
}
|
|
531
522
|
|
|
532
|
-
/**
|
|
533
|
-
* While a tool's preview is still streaming, a block whose preview is
|
|
534
|
-
* append-only (rows only grow at the bottom, never re-layout) lets the
|
|
535
|
-
* renderer commit the scrolled-off head of an over-tall preview to native
|
|
536
|
-
* scrollback instead of dropping it — the same anti-yank path a streaming
|
|
537
|
-
* assistant reply uses (see {@link TranscriptContainer} +
|
|
538
|
-
* `NativeScrollbackLiveRegion`). Covers both phases: a pre-result call preview
|
|
539
|
-
* (a `write` whose content streams in) and a partial-result preview that
|
|
540
|
-
* streams output below fixed input (an `eval`/`bash` whose stdout grows under
|
|
541
|
-
* its code cell). Gated on {@link isTranscriptBlockFinalized} so the boundary
|
|
542
|
-
* closes the instant the block reaches a terminal state — a final result that
|
|
543
|
-
* may collapse to a compact view, a backgrounded async tool, or a seal — and
|
|
544
|
-
* the renderer decides whether its current preview shape qualifies via
|
|
545
|
-
* `isStreamingPreviewAppendOnly` (typically: only the expanded full view,
|
|
546
|
-
* which is top-anchored; the collapsed tail window re-layouts but is bounded
|
|
547
|
-
* so it never overflows anyway).
|
|
548
|
-
*/
|
|
549
|
-
isTranscriptBlockAppendOnly(): boolean {
|
|
550
|
-
// A finalized block's preview can collapse/re-layout; only a live,
|
|
551
|
-
// still-streaming block is a candidate.
|
|
552
|
-
if (this.isTranscriptBlockFinalized()) return false;
|
|
553
|
-
const predicate =
|
|
554
|
-
(this.#tool as { isStreamingPreviewAppendOnly?: ToolRenderer["isStreamingPreviewAppendOnly"] } | undefined)
|
|
555
|
-
?.isStreamingPreviewAppendOnly ?? toolRenderers[this.#toolName]?.isStreamingPreviewAppendOnly;
|
|
556
|
-
if (!predicate) return false;
|
|
557
|
-
try {
|
|
558
|
-
return predicate(this.#getCallArgsForRender(), this.#renderState, this.#result);
|
|
559
|
-
} catch (err) {
|
|
560
|
-
logger.warn("Tool append-only predicate failed", { tool: this.#toolName, error: String(err) });
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
523
|
/**
|
|
566
524
|
* Mark the tool terminal even though no result arrived (the turn aborted or
|
|
567
525
|
* abandoned it) and stop animating, so it can freeze and stops pinning the
|
|
@@ -6,6 +6,8 @@ interface FrozenRender {
|
|
|
6
6
|
width: number;
|
|
7
7
|
lines: string[];
|
|
8
8
|
generation: number;
|
|
9
|
+
appendOnly: boolean;
|
|
10
|
+
volatile: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
interface SnapshotCarrier {
|
|
@@ -17,16 +19,9 @@ interface SnapshotCarrier {
|
|
|
17
19
|
* result, an assistant message mid-stream) reports `false` so the container
|
|
18
20
|
* keeps it inside the live (repaintable) region instead of freezing it. Blocks
|
|
19
21
|
* without the method are treated as finalized — the default, stable behavior.
|
|
20
|
-
*
|
|
21
|
-
* `isTranscriptBlockAppendOnly` marks a still-live block whose rendered rows
|
|
22
|
-
* only grow at the bottom and never re-layout (a streaming assistant reply).
|
|
23
|
-
* Such a block's scrolled-off head is safe to commit to native scrollback even
|
|
24
|
-
* while live; blocks that omit it (tool previews that collapse to a compact
|
|
25
|
-
* result) keep their mutable rows deferred. Default is `false`.
|
|
26
22
|
*/
|
|
27
23
|
interface FinalizableBlock {
|
|
28
24
|
isTranscriptBlockFinalized?(): boolean;
|
|
29
|
-
isTranscriptBlockAppendOnly?(): boolean;
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
function isBlockFinalized(child: Component): boolean {
|
|
@@ -34,11 +29,6 @@ function isBlockFinalized(child: Component): boolean {
|
|
|
34
29
|
return fn ? fn.call(child) : true;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
function isBlockAppendOnly(child: Component): boolean {
|
|
38
|
-
const fn = (child as Component & FinalizableBlock).isTranscriptBlockAppendOnly;
|
|
39
|
-
return fn ? fn.call(child) : false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
// A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
|
|
43
33
|
// separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
|
|
44
34
|
// to a background-colored padding row, whose escape sequences contain `\S` and
|
|
@@ -59,6 +49,73 @@ function stripPlainBlankEdges(lines: string[]): string[] {
|
|
|
59
49
|
return start === 0 && end === lines.length ? lines : lines.slice(start, end);
|
|
60
50
|
}
|
|
61
51
|
|
|
52
|
+
interface LiveCommitState {
|
|
53
|
+
appendOnly: boolean;
|
|
54
|
+
volatile: boolean;
|
|
55
|
+
safeLength: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasValidSnapshot(
|
|
59
|
+
snapshot: FrozenRender | undefined,
|
|
60
|
+
width: number,
|
|
61
|
+
generation: number,
|
|
62
|
+
): snapshot is FrozenRender {
|
|
63
|
+
return snapshot !== undefined && snapshot.generation === generation && snapshot.width === width;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function commonPrefixLength(prev: string[], cur: string[]): number {
|
|
67
|
+
const limit = Math.min(prev.length, cur.length);
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < limit && prev[i] === cur[i]) i++;
|
|
70
|
+
return i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function commonSuffixLength(prev: string[], cur: string[], prefixLength: number): number {
|
|
74
|
+
const prevLimit = prev.length - prefixLength;
|
|
75
|
+
const curLimit = cur.length - prefixLength;
|
|
76
|
+
const limit = Math.min(prevLimit, curLimit);
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (i < limit && prev[prev.length - 1 - i] === cur[cur.length - 1 - i]) i++;
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function deriveLiveCommitState(
|
|
83
|
+
previous: FrozenRender | undefined,
|
|
84
|
+
current: string[],
|
|
85
|
+
width: number,
|
|
86
|
+
generation: number,
|
|
87
|
+
): LiveCommitState {
|
|
88
|
+
let appendOnly = false;
|
|
89
|
+
let volatile = false;
|
|
90
|
+
if (hasValidSnapshot(previous, width, generation)) {
|
|
91
|
+
appendOnly = previous.appendOnly;
|
|
92
|
+
volatile = previous.volatile;
|
|
93
|
+
|
|
94
|
+
const prefixLength = commonPrefixLength(previous.lines, current);
|
|
95
|
+
const staticRender = prefixLength === previous.lines.length && prefixLength === current.length;
|
|
96
|
+
if (!staticRender) {
|
|
97
|
+
const suffixLength = commonSuffixLength(previous.lines, current, prefixLength);
|
|
98
|
+
const stablePreviousLength = prefixLength + suffixLength;
|
|
99
|
+
const appendGrew =
|
|
100
|
+
previous.lines.length > 0 &&
|
|
101
|
+
current.length > previous.lines.length &&
|
|
102
|
+
stablePreviousLength >= previous.lines.length;
|
|
103
|
+
if (appendGrew && !volatile) {
|
|
104
|
+
appendOnly = true;
|
|
105
|
+
} else if (stablePreviousLength < previous.lines.length) {
|
|
106
|
+
volatile = true;
|
|
107
|
+
appendOnly = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
appendOnly,
|
|
114
|
+
volatile,
|
|
115
|
+
safeLength: volatile ? 0 : appendOnly ? current.length : 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
62
119
|
/**
|
|
63
120
|
* Transcript container that freezes the rendered output of every block except
|
|
64
121
|
* the bottom-most (live) one on terminals where committed native scrollback is
|
|
@@ -97,11 +154,10 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
97
154
|
// render. TUI extends the native-scrollback pinned region from this point
|
|
98
155
|
// through the live blocks and the root chrome rendered below them.
|
|
99
156
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
100
|
-
// Local line index up to which the leading run of live blocks is
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// the first live block is volatile (a tool preview).
|
|
157
|
+
// Local line index up to which the leading run of live blocks is safe to
|
|
158
|
+
// commit. Finalized blocks contribute their full frozen body; still-live
|
|
159
|
+
// blocks contribute only after their stripped render has been observed
|
|
160
|
+
// growing without changing a previously rendered interior row.
|
|
105
161
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
106
162
|
|
|
107
163
|
override invalidate(): void {
|
|
@@ -164,8 +220,9 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
164
220
|
if (risk) this.#prevLiveStartIndex = liveStartIndex;
|
|
165
221
|
|
|
166
222
|
const lines: string[] = [];
|
|
167
|
-
// Tracks whether we are still inside the leading run of
|
|
168
|
-
// blocks. The first
|
|
223
|
+
// Tracks whether we are still inside the leading run of commit-safe live
|
|
224
|
+
// blocks. The first still-live volatile block closes it, but rendering
|
|
225
|
+
// continues so lower blocks remain visible.
|
|
169
226
|
let commitSafeOpen = true;
|
|
170
227
|
// The live-region start is recorded at the first visible row at/after the
|
|
171
228
|
// cutoff; empty leading blocks (or a separator) must not claim it early.
|
|
@@ -179,24 +236,41 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
179
236
|
// instead of recomputing; a stale generation (post-thaw) or width
|
|
180
237
|
// mismatch (resize) recomputes, as does a block still live last frame.
|
|
181
238
|
let contribution: string[] | undefined;
|
|
239
|
+
const previousSnapshot = risk ? child[kSnapshot] : undefined;
|
|
182
240
|
if (risk && i < liveStartIndex && i < replayCutoff) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
contribution = snapshot.lines;
|
|
241
|
+
if (hasValidSnapshot(previousSnapshot, width, this.#generation)) {
|
|
242
|
+
contribution = previousSnapshot.lines;
|
|
186
243
|
}
|
|
187
244
|
}
|
|
245
|
+
let liveCommitState: LiveCommitState | undefined;
|
|
188
246
|
if (contribution === undefined) {
|
|
189
247
|
const rendered = child.render(width);
|
|
190
248
|
contribution = stripPlainBlankEdges(rendered);
|
|
249
|
+
if (risk && i >= liveStartIndex && !isBlockFinalized(child)) {
|
|
250
|
+
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
251
|
+
}
|
|
191
252
|
// Cache every block's latest contribution. While a block is in the
|
|
192
253
|
// live region this keeps its snapshot current; on the frame it crosses
|
|
193
254
|
// out, the recompute above refreshes it before it freezes.
|
|
194
|
-
if (risk)
|
|
255
|
+
if (risk) {
|
|
256
|
+
child[kSnapshot] = {
|
|
257
|
+
width,
|
|
258
|
+
lines: contribution,
|
|
259
|
+
generation: this.#generation,
|
|
260
|
+
appendOnly: liveCommitState?.appendOnly ?? false,
|
|
261
|
+
volatile: liveCommitState?.volatile ?? false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
195
264
|
}
|
|
196
265
|
|
|
197
266
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
198
|
-
// affect spacing or the live-region offsets.
|
|
199
|
-
if
|
|
267
|
+
// affect spacing or the live-region offsets. An empty still-live child
|
|
268
|
+
// still closes the commit-safe run: if it later gains rows, it pushes
|
|
269
|
+
// everything below it.
|
|
270
|
+
if (contribution.length === 0) {
|
|
271
|
+
if (risk && i >= liveStartIndex && commitSafeOpen && !isBlockFinalized(child)) commitSafeOpen = false;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
200
274
|
|
|
201
275
|
// Every block is separated from preceding visible content by exactly one
|
|
202
276
|
// blank row — skipped when it opens the transcript or the prior row is
|
|
@@ -212,17 +286,19 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
212
286
|
}
|
|
213
287
|
|
|
214
288
|
if (sep) lines.push("");
|
|
289
|
+
const blockStart = lines.length;
|
|
215
290
|
for (let j = 0; j < contribution.length; j++) lines.push(contribution[j]!);
|
|
216
291
|
|
|
217
|
-
// Extend the commit-safe boundary through each leading append-only live
|
|
218
|
-
// block. The first volatile live block closes the run so its mutable
|
|
219
|
-
// rows stay deferred.
|
|
220
292
|
if (risk && i >= liveStartIndex && commitSafeOpen) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
293
|
+
const finalized = isBlockFinalized(child);
|
|
294
|
+
const safeLength = finalized ? contribution.length : (liveCommitState?.safeLength ?? 0);
|
|
295
|
+
if (safeLength > 0) {
|
|
296
|
+
this.#nativeScrollbackCommitSafeEnd = blockStart + safeLength;
|
|
225
297
|
}
|
|
298
|
+
// A finalized, fully safe block may let the contiguous safe run extend
|
|
299
|
+
// into blocks rendered below it. A still-live block keeps pushing lower
|
|
300
|
+
// rows around as it grows, so the run closes there.
|
|
301
|
+
if (!(finalized && safeLength >= contribution.length)) commitSafeOpen = false;
|
|
226
302
|
}
|
|
227
303
|
}
|
|
228
304
|
return lines;
|
|
@@ -720,7 +720,12 @@ export class EventController {
|
|
|
720
720
|
// seal it so it freezes (and stops animating) rather than lingering in
|
|
721
721
|
// the transcript live region as a streaming preview until the next thaw.
|
|
722
722
|
const component = this.ctx.pendingTools.get(toolCallId);
|
|
723
|
-
|
|
723
|
+
// A foreground read still pending at turn end shares a group component
|
|
724
|
+
// keyed by every read's id; seal it too so a never-delivered read does
|
|
725
|
+
// not keep the group live (and pinning the live region) indefinitely.
|
|
726
|
+
if (component instanceof ToolExecutionComponent || component instanceof ReadToolGroupComponent) {
|
|
727
|
+
component.seal();
|
|
728
|
+
}
|
|
724
729
|
this.ctx.pendingTools.delete(toolCallId);
|
|
725
730
|
}
|
|
726
731
|
}
|
|
@@ -10,6 +10,7 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
|
10
10
|
import { materializeImageReferenceLinks } from "../../modes/image-references";
|
|
11
11
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
12
12
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
13
|
+
import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
|
|
13
14
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
14
15
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
15
16
|
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
@@ -286,14 +287,21 @@ export class InputController {
|
|
|
286
287
|
|
|
287
288
|
if (!text) return;
|
|
288
289
|
|
|
289
|
-
// Continue shortcuts: "." or "c"
|
|
290
|
+
// Continue shortcuts: "." or "c" resume the agent with a hidden agent-authored
|
|
291
|
+
// developer directive (no visible user message) instead of an empty turn, so the
|
|
292
|
+
// model continues the prior intent rather than second-guessing the interrupt.
|
|
290
293
|
if (text === "." || text === "c") {
|
|
291
294
|
if (this.ctx.onInputCallback) {
|
|
292
295
|
this.ctx.editor.setText("");
|
|
293
296
|
this.ctx.pendingImages = [];
|
|
294
297
|
this.ctx.pendingImageLinks = [];
|
|
295
298
|
this.ctx.editor.imageLinks = undefined;
|
|
296
|
-
this.ctx.onInputCallback({
|
|
299
|
+
this.ctx.onInputCallback({
|
|
300
|
+
text: manualContinuePrompt,
|
|
301
|
+
cancelled: false,
|
|
302
|
+
started: true,
|
|
303
|
+
synthetic: true,
|
|
304
|
+
});
|
|
297
305
|
}
|
|
298
306
|
return;
|
|
299
307
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -42,6 +42,10 @@ export type SubmittedUserInput = {
|
|
|
42
42
|
images?: ImageContent[];
|
|
43
43
|
imageLinks?: (string | undefined)[];
|
|
44
44
|
customType?: string;
|
|
45
|
+
/** Route through `session.prompt(text, { synthetic: true })` so the text lands
|
|
46
|
+
* as a hidden agent-authored `developer` message rather than a visible user
|
|
47
|
+
* turn. Used by the `c`/`.` continue shortcut. */
|
|
48
|
+
synthetic?: boolean;
|
|
45
49
|
display?: boolean;
|
|
46
50
|
cancelled: boolean;
|
|
47
51
|
started: boolean;
|
|
@@ -10,6 +10,10 @@ import { CompactionSummaryMessageComponent } from "../../modes/components/compac
|
|
|
10
10
|
import { CustomMessageComponent } from "../../modes/components/custom-message";
|
|
11
11
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
12
12
|
import { EvalExecutionComponent } from "../../modes/components/eval-execution";
|
|
13
|
+
import {
|
|
14
|
+
type LateDiagnosticsFile,
|
|
15
|
+
LateDiagnosticsMessageComponent,
|
|
16
|
+
} from "../../modes/components/late-diagnostics-message";
|
|
13
17
|
import {
|
|
14
18
|
ReadToolGroupComponent,
|
|
15
19
|
readArgsHaveTarget,
|
|
@@ -25,6 +29,7 @@ import type { CompactionQueuedMessage, InteractiveModeContext } from "../../mode
|
|
|
25
29
|
import {
|
|
26
30
|
type CustomMessage,
|
|
27
31
|
isSilentAbort,
|
|
32
|
+
LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
|
|
28
33
|
resolveAbortLabel,
|
|
29
34
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
30
35
|
type SkillPromptDetails,
|
|
@@ -168,6 +173,17 @@ export class UiHelpers {
|
|
|
168
173
|
this.ctx.chatContainer.addChild(block);
|
|
169
174
|
break;
|
|
170
175
|
}
|
|
176
|
+
if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
|
|
177
|
+
const details = (
|
|
178
|
+
message as CustomMessage<{
|
|
179
|
+
files?: LateDiagnosticsFile[];
|
|
180
|
+
}>
|
|
181
|
+
).details;
|
|
182
|
+
const component = new LateDiagnosticsMessageComponent(details?.files ?? []);
|
|
183
|
+
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
184
|
+
this.ctx.chatContainer.addChild(component);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
171
187
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
172
188
|
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
173
189
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -342,7 +358,11 @@ export class UiHelpers {
|
|
|
342
358
|
(content.type === "thinking" && content.thinking.trim().length > 0),
|
|
343
359
|
);
|
|
344
360
|
if (hasVisibleAssistantContent) {
|
|
345
|
-
|
|
361
|
+
// Rebuild reconstructs immutable history; seal (not finalize) so the
|
|
362
|
+
// group freezes even if a read's result was never persisted —
|
|
363
|
+
// finalize alone keeps a pending entry live and would stop the whole
|
|
364
|
+
// transcript below it from committing to native scrollback.
|
|
365
|
+
readGroup?.seal();
|
|
346
366
|
readGroup = null;
|
|
347
367
|
}
|
|
348
368
|
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
|
|
@@ -392,7 +412,7 @@ export class UiHelpers {
|
|
|
392
412
|
continue;
|
|
393
413
|
}
|
|
394
414
|
|
|
395
|
-
readGroup?.
|
|
415
|
+
readGroup?.seal();
|
|
396
416
|
readGroup = null;
|
|
397
417
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
398
418
|
const renderArgs =
|
|
@@ -480,9 +500,10 @@ export class UiHelpers {
|
|
|
480
500
|
}
|
|
481
501
|
}
|
|
482
502
|
|
|
483
|
-
// The trailing read run has no following break to close it;
|
|
484
|
-
// rebuilt group
|
|
485
|
-
|
|
503
|
+
// The trailing read run has no following break to close it; seal so the
|
|
504
|
+
// rebuilt group freezes (even with a never-persisted result) and commits to
|
|
505
|
+
// native scrollback like every other historical block.
|
|
506
|
+
readGroup?.seal();
|
|
486
507
|
|
|
487
508
|
// Render deferred messages (compaction summaries) at the bottom so they're visible
|
|
488
509
|
for (const message of deferredMessages) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<system-notice>
|
|
2
|
+
Continue. Keep going from where you left off.
|
|
3
|
+
|
|
4
|
+
- You MUST resume the most recent intent and carry the unfinished work to completion.
|
|
5
|
+
- Interrupted mid-step? Pick it back up from where it stopped.
|
|
6
|
+
- You NEVER pause to summarize progress, re-confirm the plan, or ask whether to proceed — just continue.
|
|
7
|
+
</system-notice>
|