@pellux/goodvibes-tui 1.20.1 → 1.21.1
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 +22 -1
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +106 -22
- package/package.json +7 -7
- package/src/cli/doctor.ts +7 -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/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.ts +23 -23
- 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 +141 -20
- 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/input/settings-modal-types.ts +2 -1
- package/src/main.ts +12 -20
- package/src/providers/reasoning-effort-surface.ts +399 -0
- package/src/renderer/compositor.ts +68 -18
- 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/settings-modal-helpers.ts +53 -1
- package/src/renderer/settings-modal.ts +1 -50
- 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 +180 -11
- package/src/runtime/recovery-prompt.ts +11 -1
- 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
|
}
|
|
@@ -27,6 +27,7 @@ export type SettingsCategory =
|
|
|
27
27
|
| 'network'
|
|
28
28
|
| 'relay'
|
|
29
29
|
| 'surfaces'
|
|
30
|
+
| 'conversationGate'
|
|
30
31
|
| 'cloudflare'
|
|
31
32
|
| 'batch'
|
|
32
33
|
| 'automation'
|
|
@@ -60,7 +61,7 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
60
61
|
{ label: 'Interface', categories: ['display', 'ui', 'behavior', 'notifications', 'permissions', 'policy', 'security'] },
|
|
61
62
|
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts', 'voice'] },
|
|
62
63
|
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web', 'relay'] },
|
|
63
|
-
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'integrations', 'mcp', 'cloudflare'] },
|
|
64
|
+
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'conversationGate', 'integrations', 'mcp', 'cloudflare'] },
|
|
64
65
|
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc'] },
|
|
65
66
|
{ label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'power', 'memory', 'cache', 'telemetry', 'diagnostics', 'learning'] },
|
|
66
67
|
{ label: 'Advanced', categories: ['release', 'update', 'danger'] },
|
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';
|
|
@@ -67,7 +68,7 @@ import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
|
67
68
|
import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
|
|
68
69
|
import { isEffectiveDangerMode } from './config/index.ts';
|
|
69
70
|
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
70
|
-
import { applyComposerCapture } from './input/composer-capture.ts';
|
|
71
|
+
import { applyComposerCapture, applyAtModelDirective } from './input/composer-capture.ts';
|
|
71
72
|
import { createSessionAutoTitler } from './core/session-auto-titler.ts';
|
|
72
73
|
import { makeComposerEditorOpener } from './input/composer-editor.ts';
|
|
73
74
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
@@ -274,23 +275,10 @@ async function main() {
|
|
|
274
275
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
275
276
|
input.clearModalStack();
|
|
276
277
|
scrollLocked = true; // Re-lock on any user input
|
|
277
|
-
|
|
278
|
-
let processedText = text
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const modelId = atModelMatch[1];
|
|
282
|
-
try {
|
|
283
|
-
providerRegistry.setCurrentModel(modelId);
|
|
284
|
-
const def = providerRegistry.getCurrentModel();
|
|
285
|
-
runtime.model = def.id;
|
|
286
|
-
runtime.provider = def.provider;
|
|
287
|
-
configManager.set('provider.model', def.registryKey);
|
|
288
|
-
systemMessageRouter.high(`[Model] Switched to ${def.displayName} (${def.provider}) via @model:`);
|
|
289
|
-
} catch {
|
|
290
|
-
systemMessageRouter.high(`[Model] Unknown model: ${modelId}`);
|
|
291
|
-
}
|
|
292
|
-
processedText = processedText.replace(atModelMatch[0], '').trim();
|
|
293
|
-
}
|
|
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
|
+
});
|
|
294
282
|
// Composer capture markers: `!#` pins + sends; `#` saves a note without sending.
|
|
295
283
|
processedText = applyComposerCapture(processedText, {
|
|
296
284
|
sessionMemoryStore: ctx.services.sessionMemoryStore,
|
|
@@ -550,6 +538,7 @@ async function main() {
|
|
|
550
538
|
// Flush pending renders after updating the width provider and splash posture
|
|
551
539
|
// so the transcript and splash rebuild against the current shell layout.
|
|
552
540
|
conversation.getDisplayBlocks();
|
|
541
|
+
if (conversation.consumeSplashTransition()) compositor.requestFullRepaint(); // splash → transcript: repaint the whole viewport once
|
|
553
542
|
|
|
554
543
|
// Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
|
|
555
544
|
let overlayRows = 0;
|
|
@@ -705,8 +694,8 @@ async function main() {
|
|
|
705
694
|
// Time-bounded: onExpire repaints once the 60s disarm timer fires, so a stray keypress hours
|
|
706
695
|
// later can never trigger a real retry — see retry-affordance.ts.
|
|
707
696
|
const retryAffordance = createRetryAffordanceState({ onExpire: render });
|
|
708
|
-
const retryTurn = (notice?: string):
|
|
709
|
-
if (!retryCtx) return;
|
|
697
|
+
const retryTurn = (notice?: string): boolean => {
|
|
698
|
+
if (!retryCtx) return false; // nothing to roll back to; the caller narrates instead
|
|
710
699
|
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
711
700
|
// Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
|
|
712
701
|
// The rollback erases the failed turn's transcript — the failover notice included, which is how
|
|
@@ -715,12 +704,15 @@ async function main() {
|
|
|
715
704
|
conversation.removeMessagesAfter(count);
|
|
716
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,
|
|
723
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),
|
|
724
716
|
isApprovalPending: () => pendingPermission !== null,
|
|
725
717
|
});
|
|
726
718
|
unsubs.push(...streamResult.unsubs);
|