@pellux/goodvibes-tui 1.20.0 → 1.21.0
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 +20 -1
- package/README.md +7 -7
- package/docs/foundation-artifacts/operator-contract.json +106 -22
- package/package.json +7 -7
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/doctor.ts +7 -1
- package/src/cli/management.ts +1 -1
- package/src/cli/package-verification.ts +92 -7
- package/src/cli/status.ts +16 -0
- package/src/config/read-versioned.ts +115 -2
- package/src/core/active-model-identity.ts +153 -0
- package/src/core/conversation-event-navigation.ts +42 -0
- package/src/core/conversation-search-expansion.ts +67 -0
- package/src/core/conversation-splash-state.ts +58 -0
- package/src/core/conversation-user-receipts.ts +42 -0
- package/src/core/conversation.ts +43 -40
- package/src/core/model-routing-chip.ts +38 -18
- package/src/core/rewind-turn-anchors.ts +155 -3
- package/src/core/stream-event-wiring.ts +298 -17
- package/src/core/system-message-router.ts +22 -0
- package/src/core/transcript-journal.ts +145 -7
- package/src/daemon/handlers/inbox/cursor-store.ts +159 -3
- package/src/daemon/handlers/inbox/index.ts +14 -1
- package/src/daemon/handlers/sqlite-store.ts +160 -6
- package/src/input/command-registry.ts +8 -0
- package/src/input/commands/local-runtime.ts +6 -1
- package/src/input/commands/schedule-runtime.ts +15 -2
- package/src/input/commands/shell-core.ts +87 -38
- package/src/input/composer-capture.ts +47 -0
- package/src/input/handler-command-route.ts +4 -0
- package/src/input/handler-content-actions.ts +9 -0
- package/src/input/handler-feed-routes.ts +1 -0
- package/src/input/handler-modal-routes.ts +12 -1
- package/src/input/handler-picker-routes.ts +43 -5
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/handler-ui-state.ts +8 -3
- package/src/input/model-picker-items.ts +17 -2
- package/src/input/model-picker.ts +19 -2
- package/src/input/search.ts +183 -89
- package/src/main.ts +31 -38
- package/src/{cli → providers}/provider-classification.ts +12 -0
- package/src/providers/reasoning-effort-surface.ts +399 -0
- package/src/renderer/compositor.ts +68 -18
- package/src/renderer/header-line.ts +146 -0
- package/src/renderer/markdown-inline.ts +143 -0
- package/src/renderer/markdown-table.ts +459 -0
- package/src/renderer/markdown.ts +6 -335
- package/src/renderer/model-workspace.ts +48 -5
- package/src/renderer/shell-surface.ts +36 -0
- package/src/renderer/ui-factory.ts +25 -78
- package/src/runtime/bootstrap-command-parts.ts +36 -4
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/durability-housekeeping.ts +183 -0
- package/src/runtime/durability-services.ts +68 -11
- package/src/runtime/legacy-daemon-reconcile.ts +16 -7
- package/src/runtime/provider-fallback.ts +16 -2
- package/src/runtime/recovery-decisions.ts +295 -0
- package/src/runtime/recovery-prompt.ts +66 -6
- package/src/runtime/services.ts +9 -4
- package/src/runtime/session-liveness-marker.ts +142 -5
- package/src/runtime/workstream-draft-store.ts +198 -25
- package/src/runtime/workstream-services.ts +17 -2
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +334 -16
package/src/input/search.ts
CHANGED
|
@@ -5,6 +5,34 @@ export interface SearchMatch {
|
|
|
5
5
|
line: number;
|
|
6
6
|
col: number;
|
|
7
7
|
length: number;
|
|
8
|
+
/**
|
|
9
|
+
* Present when this match lives inside content that is still collapsed —
|
|
10
|
+
* `line`/`col` are placeholders (the containing block's own header line,
|
|
11
|
+
* col 0) rather than a real navigable position. `collapseKeys` names every
|
|
12
|
+
* collapse key that must be expanded to make the hit a real rendered
|
|
13
|
+
* line; `primaryKey` is the specific key whose block range identifies
|
|
14
|
+
* where the revealed match will land (see revealCurrentMatch).
|
|
15
|
+
* Absent entirely for a match already visible in the rendered buffer.
|
|
16
|
+
*/
|
|
17
|
+
collapseKeys?: readonly string[];
|
|
18
|
+
primaryKey?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Count non-overlapping occurrences of `needle` in `haystack` — used for
|
|
22
|
+
* the collapsed-content scan below, mirroring the per-line indexOf loop
|
|
23
|
+
* search() already runs over the rendered buffer, so a hidden hit counts
|
|
24
|
+
* the same way a visible one would once revealed. */
|
|
25
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
26
|
+
if (needle.length === 0) return 0;
|
|
27
|
+
let count = 0;
|
|
28
|
+
let col = 0;
|
|
29
|
+
while (col <= haystack.length - needle.length) {
|
|
30
|
+
const idx = haystack.indexOf(needle, col);
|
|
31
|
+
if (idx === -1) break;
|
|
32
|
+
count++;
|
|
33
|
+
col = idx + 1;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
8
36
|
}
|
|
9
37
|
|
|
10
38
|
/**
|
|
@@ -45,127 +73,187 @@ export class SearchManager {
|
|
|
45
73
|
this.locked = false;
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Close search mode. Re-collapses every block/group search auto-expanded
|
|
78
|
+
* while it was open — except ones the user explicitly acted on (toggled,
|
|
79
|
+
* copied, bookmarked, saved) while they were expanded, which stay exactly
|
|
80
|
+
* as the user left them (see SearchExpansionTracker.restoreOnto).
|
|
81
|
+
* `conversationManager` is optional so existing call sites that never had
|
|
82
|
+
* one (and therefore never auto-expanded anything) keep working unchanged.
|
|
83
|
+
*/
|
|
84
|
+
close(conversationManager?: ConversationManager | null): void {
|
|
85
|
+
conversationManager?.searchExpansion.restoreOnto(conversationManager);
|
|
50
86
|
this.active = false;
|
|
51
87
|
this.locked = false;
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
/**
|
|
55
|
-
* Update query and find matches
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* extra expand is not), so the match becomes a real, navigable line in the
|
|
63
|
-
* rendered buffer scanned below.
|
|
91
|
+
* Update query and find matches — honestly counting hits inside collapsed
|
|
92
|
+
* blocks and folded tool-result groups WITHOUT expanding them. Expansion
|
|
93
|
+
* only happens on navigation (see revealCurrentMatch): typing a query must
|
|
94
|
+
* never collapse-destroy the transcript the user folded on purpose, but a
|
|
95
|
+
* "no matches" while text the user watched stream by sits collapsed
|
|
96
|
+
* somewhere would be a lie too — so the count includes it, tagged as
|
|
97
|
+
* hidden until the user actually navigates there.
|
|
64
98
|
*
|
|
65
|
-
* That corpus is the block's own RAW content, plus — for a
|
|
66
|
-
* tool-result group — the raw content of every one of its member
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* read off the message snapshot, and is the same string the renderer stores
|
|
73
|
-
* as that member's own block rawContent once the group is expanded.
|
|
99
|
+
* That collapsed corpus is the block's own RAW content, plus — for a
|
|
100
|
+
* folded tool-result group — the raw content of every one of its member
|
|
101
|
+
* messages (a group's own rawContent is only its summary header line, and
|
|
102
|
+
* its members push no BlockMeta of their own while folded — see
|
|
103
|
+
* conversation-tool-groups.ts — so without the member corpus a needle
|
|
104
|
+
* living inside a member would count as zero hits for text the user
|
|
105
|
+
* watched stream by).
|
|
74
106
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* `getDisplayBlocks()` is flushed before the buffer is scanned, and the
|
|
81
|
-
* overlay's match count is always derived from that same rendered scan, so
|
|
82
|
-
* it never claims more than it can actually navigate to.
|
|
107
|
+
* Matches are built in one pass over `history.getAllLines()` combined with
|
|
108
|
+
* one pass over collapsed blocks, then sorted into document order (by
|
|
109
|
+
* rendered line, with a collapsed block's hidden hits keyed to its own
|
|
110
|
+
* still-real header line) so that navigation lands on hits in the order
|
|
111
|
+
* they actually appear in the transcript.
|
|
83
112
|
*/
|
|
84
113
|
search(query: string, history: InfiniteBuffer, conversationManager?: ConversationManager | null): void {
|
|
85
114
|
this.query = query;
|
|
86
|
-
this.matches = [];
|
|
87
115
|
this.currentMatch = 0;
|
|
88
|
-
|
|
89
116
|
this.wrapAround = false;
|
|
90
|
-
if (query.length === 0) return;
|
|
91
117
|
|
|
118
|
+
if (query.length === 0) {
|
|
119
|
+
this.matches = [];
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.matches = this.buildMatches(query, history, conversationManager);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Shared match builder for search() and revealCurrentMatch() — scans the
|
|
128
|
+
* currently-rendered buffer for visible matches and every collapsed
|
|
129
|
+
* block/folded group for hidden ones, without mutating any collapse
|
|
130
|
+
* state. Kept separate from search() so revealCurrentMatch can rebuild the
|
|
131
|
+
* match list after an expansion without resetting currentMatch back to 0.
|
|
132
|
+
*/
|
|
133
|
+
private buildMatches(query: string, history: InfiniteBuffer, conversationManager?: ConversationManager | null): SearchMatch[] {
|
|
92
134
|
const lowerQuery = query.toLowerCase();
|
|
135
|
+
const hiddenMatches: SearchMatch[] = [];
|
|
93
136
|
|
|
94
137
|
if (conversationManager) {
|
|
95
138
|
const registry = conversationManager.getBlockRegistry();
|
|
96
139
|
|
|
97
|
-
// A folded tool-result group's members render nothing and register no
|
|
98
|
-
// BlockMeta while collapsed (see conversation-tool-groups.ts), so the
|
|
99
|
-
// group's searchable corpus is its own header rawContent PLUS each
|
|
100
|
-
// member's raw content — which only the message snapshot can supply,
|
|
101
|
-
// since there is no member block to read it from.
|
|
102
|
-
const memberIndexes: number[] = [];
|
|
103
140
|
for (const block of registry) {
|
|
104
|
-
if (block.type !== 'tool_group' || !block.groupMemberIndexes) continue;
|
|
105
141
|
if (!conversationManager.isCollapsed(block.blockIndex)) continue;
|
|
106
|
-
memberIndexes.push(...block.groupMemberIndexes);
|
|
107
|
-
}
|
|
108
142
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// stale registry still names it) reads back undefined and is simply
|
|
123
|
-
// skipped rather than throwing.
|
|
124
|
-
const content = snapshot[memberIdx]?.content;
|
|
125
|
-
if (typeof content === 'string') memberContent.set(memberIdx, content.toLowerCase());
|
|
126
|
-
}
|
|
127
|
-
}
|
|
143
|
+
if (block.type === 'tool_group' && block.groupMemberIndexes) {
|
|
144
|
+
// The header's own synthetic summary rarely matches, but count it
|
|
145
|
+
// honestly too — expanding the whole group is the only way to even
|
|
146
|
+
// attempt to reveal a header-corpus hit, since there is no single
|
|
147
|
+
// member to isolate it to.
|
|
148
|
+
const headerHits = countOccurrences(block.rawContent.toLowerCase(), lowerQuery);
|
|
149
|
+
const allKeys = [block.collapseKey, ...block.groupMemberIndexes.map((idx) => `msg_${idx}`)];
|
|
150
|
+
for (let i = 0; i < headerHits; i++) {
|
|
151
|
+
hiddenMatches.push({
|
|
152
|
+
line: block.startLine, col: 0, length: query.length,
|
|
153
|
+
collapseKeys: allKeys, primaryKey: block.collapseKey,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
128
156
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
if (block.groupMemberIndexes.length > 0) {
|
|
158
|
+
// Read in ONE snapshot pass: getMessageSnapshot() clones the
|
|
159
|
+
// whole message array and search() runs on every keystroke, so a
|
|
160
|
+
// per-member read would stall typing outright.
|
|
161
|
+
const snapshot = conversationManager.getMessageSnapshot();
|
|
162
|
+
for (const memberIdx of block.groupMemberIndexes) {
|
|
163
|
+
const content = snapshot[memberIdx]?.content;
|
|
164
|
+
if (typeof content !== 'string') continue;
|
|
165
|
+
const memberKey = `msg_${memberIdx}`;
|
|
166
|
+
const memberHits = countOccurrences(content.toLowerCase(), lowerQuery);
|
|
167
|
+
for (let i = 0; i < memberHits; i++) {
|
|
168
|
+
hiddenMatches.push({
|
|
169
|
+
line: block.startLine, col: 0, length: query.length,
|
|
170
|
+
// Revealing a member-specific hit only needs the group
|
|
171
|
+
// header (to unfold the group at all) and that member's own
|
|
172
|
+
// key — not its siblings, which stay exactly as they were.
|
|
173
|
+
collapseKeys: [block.collapseKey, memberKey], primaryKey: memberKey,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const hits = countOccurrences(block.rawContent.toLowerCase(), lowerQuery);
|
|
180
|
+
for (let i = 0; i < hits; i++) {
|
|
181
|
+
hiddenMatches.push({
|
|
182
|
+
line: block.startLine, col: 0, length: query.length,
|
|
183
|
+
collapseKeys: [block.collapseKey], primaryKey: block.collapseKey,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
143
186
|
}
|
|
144
|
-
expandedAny = true;
|
|
145
|
-
}
|
|
146
|
-
if (expandedAny) {
|
|
147
|
-
// Flush the pending re-render so `history` (the same buffer
|
|
148
|
-
// instance) reflects the newly-expanded content before it's scanned
|
|
149
|
-
// below — mirrors the existing getDisplayBlocks()-before-read idiom
|
|
150
|
-
// used by jumpToBookmark/scrollToLine.
|
|
151
|
-
conversationManager.getDisplayBlocks();
|
|
152
187
|
}
|
|
153
188
|
}
|
|
154
189
|
|
|
190
|
+
const visibleMatches: SearchMatch[] = [];
|
|
155
191
|
const lines = history.getAllLines();
|
|
156
|
-
|
|
157
192
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
158
193
|
const line = lines[lineIdx];
|
|
159
|
-
// Build text from cells
|
|
160
194
|
const text = line.map(c => c.char).join('').toLowerCase();
|
|
161
195
|
let col = 0;
|
|
162
196
|
while (col <= text.length - lowerQuery.length) {
|
|
163
197
|
const idx = text.indexOf(lowerQuery, col);
|
|
164
198
|
if (idx === -1) break;
|
|
165
|
-
|
|
199
|
+
visibleMatches.push({ line: lineIdx, col: idx, length: query.length });
|
|
166
200
|
col = idx + 1;
|
|
167
201
|
}
|
|
168
202
|
}
|
|
203
|
+
|
|
204
|
+
// Stable sort (V8/JSC have guaranteed stable Array#sort) puts each
|
|
205
|
+
// block's hidden hits in the same relative order they were generated
|
|
206
|
+
// in — self-corpus first, then members in order — right where that
|
|
207
|
+
// block's header line falls among the visible matches.
|
|
208
|
+
return [...visibleMatches, ...hiddenMatches].sort((a, b) => a.line - b.line || a.col - b.col);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reveal the current match if it lives inside collapsed content: expands
|
|
213
|
+
* exactly the block (and, for a folded tool-result group member, its
|
|
214
|
+
* containing group) needed to make the hit a real navigable line, marks
|
|
215
|
+
* those keys as search-owned so close() can restore them, and rebuilds
|
|
216
|
+
* the match list against the now-expanded buffer.
|
|
217
|
+
*
|
|
218
|
+
* `currentMatch` is repointed at the first now-visible match inside the
|
|
219
|
+
* revealed block's new line range, rather than reset to 0 — expansion
|
|
220
|
+
* only changes how many lines that ONE block renders as, so every match
|
|
221
|
+
* belonging to earlier blocks keeps its relative position.
|
|
222
|
+
*
|
|
223
|
+
* No-op when the current match is already visible, or when no
|
|
224
|
+
* conversationManager was supplied (nothing to expand against).
|
|
225
|
+
*/
|
|
226
|
+
revealCurrentMatch(history: InfiniteBuffer, conversationManager?: ConversationManager | null): void {
|
|
227
|
+
const match = this.matches[this.currentMatch];
|
|
228
|
+
if (!match || !match.collapseKeys || match.collapseKeys.length === 0) return;
|
|
229
|
+
if (!conversationManager) return;
|
|
230
|
+
|
|
231
|
+
for (const key of match.collapseKeys) {
|
|
232
|
+
conversationManager.setCollapsed(key, false);
|
|
233
|
+
conversationManager.searchExpansion.markSearchExpanded(key);
|
|
234
|
+
}
|
|
235
|
+
// Flush so the registry read below reflects the newly-expanded content.
|
|
236
|
+
conversationManager.getDisplayBlocks();
|
|
237
|
+
|
|
238
|
+
const primaryKey = match.primaryKey ?? match.collapseKeys[match.collapseKeys.length - 1];
|
|
239
|
+
const revealedBlock = conversationManager.getBlockRegistry().find((b) => b.collapseKey === primaryKey);
|
|
240
|
+
|
|
241
|
+
this.matches = this.buildMatches(this.query, history, conversationManager);
|
|
242
|
+
|
|
243
|
+
if (revealedBlock) {
|
|
244
|
+
const idx = this.matches.findIndex((m) =>
|
|
245
|
+
!m.collapseKeys
|
|
246
|
+
&& m.line >= revealedBlock.startLine
|
|
247
|
+
&& m.line < revealedBlock.startLine + revealedBlock.lineCount,
|
|
248
|
+
);
|
|
249
|
+
if (idx >= 0) {
|
|
250
|
+
this.currentMatch = idx;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Fallback: keep the pointer in range rather than throwing if the exact
|
|
255
|
+
// target couldn't be relocated.
|
|
256
|
+
if (this.currentMatch >= this.matches.length) this.currentMatch = Math.max(0, this.matches.length - 1);
|
|
169
257
|
}
|
|
170
258
|
|
|
171
259
|
/** Jump to next match. Wraps around; sets wrapAround when it does. */
|
|
@@ -184,21 +272,27 @@ export class SearchManager {
|
|
|
184
272
|
this.currentMatch = prev;
|
|
185
273
|
}
|
|
186
274
|
|
|
187
|
-
/** Get the line number of the current match (for scroll).
|
|
275
|
+
/** Get the line number of the current match (for scroll). Returns -1 for a
|
|
276
|
+
* match that's still hidden inside collapsed content — revealCurrentMatch
|
|
277
|
+
* must run first to give it a real line. */
|
|
188
278
|
getCurrentMatchLine(): number {
|
|
189
279
|
if (this.matches.length === 0) return -1;
|
|
190
|
-
|
|
280
|
+
const match = this.matches[this.currentMatch];
|
|
281
|
+
if (!match) return -1;
|
|
282
|
+
if (match.collapseKeys && match.collapseKeys.length > 0) return -1;
|
|
283
|
+
return match.line;
|
|
191
284
|
}
|
|
192
285
|
|
|
193
|
-
/** Get matches on a given line.
|
|
286
|
+
/** Get matches on a given line. Hidden (still-collapsed) matches are
|
|
287
|
+
* excluded — they have no real line to render a highlight on yet. */
|
|
194
288
|
getMatchesOnLine(lineIdx: number): SearchMatch[] {
|
|
195
|
-
return this.matches.filter(m => m.line === lineIdx);
|
|
289
|
+
return this.matches.filter(m => m.line === lineIdx && !m.collapseKeys);
|
|
196
290
|
}
|
|
197
291
|
|
|
198
292
|
/** Is the current match on the given line at the given col? */
|
|
199
293
|
isCurrentMatch(lineIdx: number, col: number): boolean {
|
|
200
294
|
if (this.matches.length === 0) return false;
|
|
201
295
|
const m = this.matches[this.currentMatch];
|
|
202
|
-
return m !== undefined && m.line === lineIdx && m.col === col;
|
|
296
|
+
return m !== undefined && !m.collapseKeys && m.line === lineIdx && m.col === col;
|
|
203
297
|
}
|
|
204
298
|
}
|
package/src/main.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { PermissionPromptUI, buildPendingPermissionExtras } from './permissions/
|
|
|
16
16
|
import { handleBrokerApprovalChange, buildFixSessionAffordance, buildFixSessionErrorNotice, handleFixSessionAttachKey, refreshFixSessionsFromApprovals } from './permissions/broker-approval-card.ts';
|
|
17
17
|
import { CommandRegistry } from './input/command-registry.ts';
|
|
18
18
|
import type { CommandContext } from './input/command-registry.ts';
|
|
19
|
+
import { requestedEffortLevel } from './providers/reasoning-effort-surface.ts';
|
|
19
20
|
import { renderProcessIndicator } from './renderer/process-indicator.ts';
|
|
20
21
|
import { registerBuiltinCommands } from './input/commands.ts';
|
|
21
22
|
import { ScheduleManager } from '@pellux/goodvibes-sdk/platform/tools';
|
|
@@ -24,7 +25,8 @@ import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvi
|
|
|
24
25
|
import { GitStatusProvider } from './renderer/git-status.ts';
|
|
25
26
|
import type { GitHeaderInfo } from './renderer/git-status.ts';
|
|
26
27
|
import { createShellLayout } from './renderer/layout-engine.ts';
|
|
27
|
-
import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
|
|
28
|
+
import { buildShellFooter, estimateShellFooterHeight, promptCursorOffset } from './renderer/shell-surface.ts';
|
|
29
|
+
import { createFailoverTurnState, resolveActiveModelDisplay } from './core/active-model-identity.ts';
|
|
28
30
|
import { computePromptContentWidth } from './renderer/prompt-content-width.ts';
|
|
29
31
|
import { buildConversationViewport } from './renderer/conversation-layout.ts';
|
|
30
32
|
import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
|
|
@@ -66,7 +68,7 @@ import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
|
66
68
|
import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
|
|
67
69
|
import { isEffectiveDangerMode } from './config/index.ts';
|
|
68
70
|
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
69
|
-
import { applyComposerCapture } from './input/composer-capture.ts';
|
|
71
|
+
import { applyComposerCapture, applyAtModelDirective } from './input/composer-capture.ts';
|
|
70
72
|
import { createSessionAutoTitler } from './core/session-auto-titler.ts';
|
|
71
73
|
import { makeComposerEditorOpener } from './input/composer-editor.ts';
|
|
72
74
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
@@ -178,6 +180,8 @@ async function main() {
|
|
|
178
180
|
let lastMaxScroll: number | null = null;
|
|
179
181
|
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
180
182
|
const streamMetrics: StreamMetrics = createStreamMetrics();
|
|
183
|
+
// Live failover record — written by the failover path, read every frame so the header and footer agree.
|
|
184
|
+
const failoverState = createFailoverTurnState();
|
|
181
185
|
|
|
182
186
|
const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
|
|
183
187
|
|
|
@@ -271,23 +275,10 @@ async function main() {
|
|
|
271
275
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
272
276
|
input.clearModalStack();
|
|
273
277
|
scrollLocked = true; // Re-lock on any user input
|
|
274
|
-
|
|
275
|
-
let processedText = text
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const modelId = atModelMatch[1];
|
|
279
|
-
try {
|
|
280
|
-
providerRegistry.setCurrentModel(modelId);
|
|
281
|
-
const def = providerRegistry.getCurrentModel();
|
|
282
|
-
runtime.model = def.id;
|
|
283
|
-
runtime.provider = def.provider;
|
|
284
|
-
configManager.set('provider.model', def.registryKey);
|
|
285
|
-
systemMessageRouter.high(`[Model] Switched to ${def.displayName} (${def.provider}) via @model:`);
|
|
286
|
-
} catch {
|
|
287
|
-
systemMessageRouter.high(`[Model] Unknown model: ${modelId}`);
|
|
288
|
-
}
|
|
289
|
-
processedText = processedText.replace(atModelMatch[0], '').trim();
|
|
290
|
-
}
|
|
278
|
+
conversation.dismissSplash(); // owner rule: any submission retires the splash for the run
|
|
279
|
+
let processedText = applyAtModelDirective(text, {
|
|
280
|
+
providerRegistry, runtime, configManager, notify: (m) => systemMessageRouter.high(m),
|
|
281
|
+
});
|
|
291
282
|
// Composer capture markers: `!#` pins + sends; `#` saves a note without sending.
|
|
292
283
|
processedText = applyComposerCapture(processedText, {
|
|
293
284
|
sessionMemoryStore: ctx.services.sessionMemoryStore,
|
|
@@ -454,10 +445,9 @@ async function main() {
|
|
|
454
445
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
455
446
|
const agentSnapshot = uiServices.readModels.agents.getSnapshot();
|
|
456
447
|
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
);
|
|
448
|
+
const activeModel = resolveActiveModelDisplay({ serving: currentModel, configuredRegistryKey: configManager.get('provider.model') as string, configuredLabel: runtime.model, configuredProvider: runtime.provider, failover: failoverState.current() });
|
|
449
|
+
const headerLines = UIFactory.createHeader(width, activeModel.headerModel, activeModel.headerProvider, conversation.title || undefined, lastGitInfoRef.value, undefined, activeModel.divergenceNote);
|
|
450
|
+
const managerAgents = agentManager.list().filter((a) => a.status === 'running' || a.status === 'pending');
|
|
461
451
|
const runtimeAgents = agentSnapshot.active;
|
|
462
452
|
const runningAgentSummary = summarizeRunningAgents(managerAgents, runtimeAgents, ctx.services.wrfcController.listChains());
|
|
463
453
|
const runningAgentCount = runningAgentSummary.count;
|
|
@@ -470,30 +460,24 @@ async function main() {
|
|
|
470
460
|
commandMode: input.commandMode,
|
|
471
461
|
panelFocused: input.panelFocused,
|
|
472
462
|
pendingApproval: pendingPermission !== null,
|
|
473
|
-
hasAttachments: input.getImageAttachments().size > 0,
|
|
474
|
-
turnState: sessionSnapshot.turnState,
|
|
463
|
+
hasAttachments: input.getImageAttachments().size > 0, turnState: sessionSnapshot.turnState,
|
|
475
464
|
});
|
|
476
465
|
const contextStatusHint = resolveContextStatusHint({
|
|
477
466
|
evaluate: (args) => evaluateSessionMaintenance({ configManager, ...args, sessionMemoryCount: ctx.services.sessionMemoryStore.list().length }),
|
|
478
|
-
currentTokens: orchestrator.lastInputTokens,
|
|
479
|
-
contextWindow,
|
|
467
|
+
currentTokens: orchestrator.lastInputTokens, contextWindow,
|
|
480
468
|
});
|
|
481
469
|
const footerLines = buildShellFooter({
|
|
482
470
|
width,
|
|
483
471
|
promptText: promptInfo.visibleLines.join('\n'),
|
|
484
472
|
promptLineCount: promptInfo.visibleLines.length,
|
|
485
|
-
promptCursorPos: promptInfo
|
|
486
|
-
? promptInfo.visibleLines
|
|
487
|
-
.slice(0, promptInfo.visibleCursorLine)
|
|
488
|
-
.reduce((sum: number, line: string) => sum + line.length + 1, 0) + promptInfo.visibleCursorCol
|
|
489
|
-
: undefined,
|
|
473
|
+
promptCursorPos: promptCursorOffset(promptInfo),
|
|
490
474
|
usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
|
|
491
475
|
showExitNotice: input.showExitNotice,
|
|
492
476
|
lastCopyTime: input.lastCopyTime,
|
|
493
|
-
model:
|
|
477
|
+
model: activeModel.footerModel, modelNote: activeModel.divergenceNote,
|
|
494
478
|
toolCount: toolRegistry.list().length,
|
|
495
479
|
workingDir,
|
|
496
|
-
provider:
|
|
480
|
+
provider: activeModel.footerProvider,
|
|
497
481
|
contextWindow,
|
|
498
482
|
contextStatusHint,
|
|
499
483
|
retryHint: retryAffordanceHint(retryAffordance),
|
|
@@ -554,6 +538,7 @@ async function main() {
|
|
|
554
538
|
// Flush pending renders after updating the width provider and splash posture
|
|
555
539
|
// so the transcript and splash rebuild against the current shell layout.
|
|
556
540
|
conversation.getDisplayBlocks();
|
|
541
|
+
if (conversation.consumeSplashTransition()) compositor.requestFullRepaint(); // splash → transcript: repaint the whole viewport once
|
|
557
542
|
|
|
558
543
|
// Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
|
|
559
544
|
let overlayRows = 0;
|
|
@@ -709,17 +694,25 @@ async function main() {
|
|
|
709
694
|
// Time-bounded: onExpire repaints once the 60s disarm timer fires, so a stray keypress hours
|
|
710
695
|
// later can never trigger a real retry — see retry-affordance.ts.
|
|
711
696
|
const retryAffordance = createRetryAffordanceState({ onExpire: render });
|
|
712
|
-
const retryTurn = ():
|
|
713
|
-
if (!retryCtx) return;
|
|
697
|
+
const retryTurn = (notice?: string): boolean => {
|
|
698
|
+
if (!retryCtx) return false; // nothing to roll back to; the caller narrates instead
|
|
714
699
|
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
715
700
|
// Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
|
|
701
|
+
// The rollback erases the failed turn's transcript — the failover notice included, which is how
|
|
702
|
+
// that notice used to vanish before anyone could read it. The caller hands it over instead, and
|
|
703
|
+
// it is posted here: after the rollback, above the prompt it explains.
|
|
716
704
|
conversation.removeMessagesAfter(count);
|
|
705
|
+
if (notice) systemMessageRouter.userReceipt(notice);
|
|
717
706
|
void refreshMemoryRecallSnapshot(ctx.services).then(() => orchestrator.handleUserInput(text, rContent, rOpts)).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
|
|
707
|
+
return true;
|
|
718
708
|
};
|
|
719
709
|
const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
|
|
720
710
|
events: uiServices.events, orchestrator, providerRegistry,
|
|
721
711
|
systemMessageRouter, render, metrics: streamMetrics,
|
|
722
712
|
providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn,
|
|
713
|
+
failoverState, getConfiguredRegistryKey: () => configManager.get('provider.model') as string | undefined,
|
|
714
|
+
// The REQUESTED level, through the one helper every remap site reads from.
|
|
715
|
+
getConfiguredReasoningEffort: () => requestedEffortLevel(configManager),
|
|
723
716
|
isApprovalPending: () => pendingPermission !== null,
|
|
724
717
|
});
|
|
725
718
|
unsubs.push(...streamResult.unsubs);
|
|
@@ -743,7 +736,7 @@ async function main() {
|
|
|
743
736
|
const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
|
|
744
737
|
|
|
745
738
|
// continueRecovery lets --continue/bare --resume check the target session for a live crash snapshot newer than its store before resuming (see tui-startup.ts).
|
|
746
|
-
applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, surface: ctx.services.surface, render, continueRecovery: { sessionManager: ctx.services.sessionManager, runtime, conversation, writeLastSessionPointer, receipt: (line) => systemMessageRouter.
|
|
739
|
+
applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, surface: ctx.services.surface, render, continueRecovery: { sessionManager: ctx.services.sessionManager, runtime, conversation, writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line) } });
|
|
747
740
|
|
|
748
741
|
stdin.on('data', (raw: string) => {
|
|
749
742
|
const data = themeProbe.filterInput(raw); if (data.length === 0) return;
|
|
@@ -788,7 +781,7 @@ async function main() {
|
|
|
788
781
|
// frame so the question is drawn rather than posed at a blank terminal.
|
|
789
782
|
scheduleRecoveryOffer(buildRecoveryOfferWiring({
|
|
790
783
|
surface: ctx.services.surface, sessionManager: ctx.services.sessionManager, runtime, conversation, commandContext,
|
|
791
|
-
writeLastSessionPointer, receipt: (line) => systemMessageRouter.
|
|
784
|
+
writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line), render,
|
|
792
785
|
}));
|
|
793
786
|
|
|
794
787
|
// Auto-save to recovery file every 60s + multi-instance liveness-marker
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-classification.ts — how a provider is paid for: a raw API key, a
|
|
3
|
+
* cloud account, a stored subscription/OAuth session, a local runtime, or
|
|
4
|
+
* honestly unknown.
|
|
5
|
+
*
|
|
6
|
+
* Lives in the foundation `providers` layer (it imports nothing) because both
|
|
7
|
+
* the CLI management surfaces and the in-session failover notice need it: when
|
|
8
|
+
* automatic failover moves a turn onto a different backend, the notice names
|
|
9
|
+
* the billing class it moved from and to, so a switch from a metered API key
|
|
10
|
+
* onto the user's own subscription is something the user can see and object to
|
|
11
|
+
* rather than discover on an invoice.
|
|
12
|
+
*/
|
|
1
13
|
export type ProviderSetupClass =
|
|
2
14
|
| 'api-key'
|
|
3
15
|
| 'cloud-account'
|