@crouton-kit/humanloop 0.3.38 → 0.4.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/dist/api.d.ts +3 -2
- package/dist/api.js +2 -2
- package/dist/browser/server.d.ts +15 -5
- package/dist/browser/server.js +110 -14
- package/dist/cli.js +0 -0
- package/dist/editor/roundtrip.d.ts +5 -0
- package/dist/editor/roundtrip.js +36 -0
- package/dist/inbox/completion.js +1 -27
- package/dist/inbox/controller.d.ts +32 -3
- package/dist/inbox/controller.js +427 -35
- package/dist/inbox/convention.d.ts +9 -2
- package/dist/inbox/convention.js +51 -3
- package/dist/inbox/deck-adapter.d.ts +10 -2
- package/dist/inbox/deck-adapter.js +23 -12
- package/dist/inbox/deck-schema.d.ts +2 -0
- package/dist/inbox/deck-schema.js +4 -4
- package/dist/inbox/followup.d.ts +57 -0
- package/dist/inbox/followup.js +117 -0
- package/dist/inbox/registry.d.ts +4 -0
- package/dist/inbox/registry.js +9 -4
- package/dist/inbox/tickets.d.ts +5 -0
- package/dist/inbox/tickets.js +25 -32
- package/dist/inbox/tui.d.ts +1 -1
- package/dist/inbox/tui.js +58 -25
- package/dist/inbox/visual.d.ts +130 -0
- package/dist/inbox/visual.js +747 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/tui/app.d.ts +3 -6
- package/dist/tui/app.js +130 -97
- package/dist/tui/input.d.ts +5 -1
- package/dist/tui/input.js +41 -15
- package/dist/tui/log.d.ts +2 -0
- package/dist/tui/log.js +53 -0
- package/dist/tui/render.js +44 -27
- package/dist/tui/terminal.d.ts +1 -0
- package/dist/tui/terminal.js +5 -0
- package/dist/tui/tmux.js +44 -22
- package/dist/types.d.ts +71 -7
- package/dist/web/assets/{index-DhbBiRqS.js → index-YFwgZZMg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +6 -3
- package/dist/conversation/reader.d.ts +0 -6
- package/dist/conversation/reader.js +0 -58
- package/dist/visuals/generate.d.ts +0 -9
- package/dist/visuals/generate.js +0 -79
package/dist/tui/render.js
CHANGED
|
@@ -134,30 +134,46 @@ function buildItemReviewLayout(state, cols, rows) {
|
|
|
134
134
|
// and live in the scrollable region so long content never overflows the fixed
|
|
135
135
|
// header — agents put rich prose in either field, so both must render markdown.
|
|
136
136
|
const bodyLines = [];
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if (state.bodyMode === 'question') {
|
|
138
|
+
if (interaction.subtitle) {
|
|
139
|
+
bodyLines.push('');
|
|
140
|
+
for (const line of renderMarkdown(interaction.subtitle, maxW)) {
|
|
141
|
+
bodyLines.push(` ${line}`);
|
|
142
|
+
}
|
|
141
143
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
if (interaction.body) {
|
|
145
|
+
bodyLines.push('');
|
|
146
|
+
for (const line of renderMarkdown(interaction.body, maxW)) {
|
|
147
|
+
bodyLines.push(` ${line}`);
|
|
148
|
+
}
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
if (state.bodyMode === 'visual') {
|
|
152
|
+
if (visual?.status === 'ready') {
|
|
153
|
+
bodyLines.push('');
|
|
154
|
+
bodyLines.push(` ${DIM}── context ${hline(maxW - 12)}${RESET}`);
|
|
155
|
+
for (const vl of visual.content.split('\n')) {
|
|
156
|
+
bodyLines.push(` ${vl}`);
|
|
157
|
+
}
|
|
158
|
+
bodyLines.push(` ${DIM}${hline(maxW)}${RESET}`);
|
|
159
|
+
}
|
|
160
|
+
if (state.followUp !== undefined && state.followUp.status !== 'idle') {
|
|
161
|
+
bodyLines.push('');
|
|
162
|
+
bodyLines.push(` ${DIM}── follow-up ${hline(Math.max(0, maxW - 14))}${RESET}`);
|
|
163
|
+
if (state.followUp.status === 'running')
|
|
164
|
+
bodyLines.push(` ${DIM}consulting…${RESET}`);
|
|
165
|
+
else if (state.followUp.status === 'ready') {
|
|
166
|
+
for (const line of renderMarkdown(state.followUp.markdown, maxW))
|
|
167
|
+
bodyLines.push(` ${line}`);
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
bodyLines.push(` ${YELLOW}${state.followUp.error}${RESET}`);
|
|
154
171
|
}
|
|
155
|
-
bodyLines.push(` ${DIM}${hline(maxW)}${RESET}`);
|
|
156
172
|
}
|
|
157
173
|
// Post-body: visual status hint, input buffer or actions, footer (always visible)
|
|
158
174
|
const postLines = [];
|
|
159
175
|
postLines.push('');
|
|
160
|
-
if (visual) {
|
|
176
|
+
if (state.bodyMode === 'visual' && visual) {
|
|
161
177
|
if (visual.status === 'loading') {
|
|
162
178
|
postLines.push(` ${DIM}loading context...${RESET}`);
|
|
163
179
|
postLines.push('');
|
|
@@ -166,10 +182,6 @@ function buildItemReviewLayout(state, cols, rows) {
|
|
|
166
182
|
postLines.push(` ${YELLOW}visual context unavailable${RESET}`);
|
|
167
183
|
postLines.push('');
|
|
168
184
|
}
|
|
169
|
-
else if (!state.detailExpanded) {
|
|
170
|
-
postLines.push(` ${DIM}[space] expand context${RESET}`);
|
|
171
|
-
postLines.push('');
|
|
172
|
-
}
|
|
173
185
|
}
|
|
174
186
|
if (state.inputMode) {
|
|
175
187
|
postLines.push(` ${DIM}${hline(maxW)}${RESET}`);
|
|
@@ -204,10 +216,10 @@ function buildItemReviewLayout(state, cols, rows) {
|
|
|
204
216
|
postLines.push(attachedLine);
|
|
205
217
|
}
|
|
206
218
|
postLines.push('');
|
|
207
|
-
postLines.push(` ${DIM}enter${RESET} submit ${DIM}^J/⌥⏎${RESET} newline ${DIM}^O${RESET} editor ${DIM}esc${RESET} cancel`);
|
|
219
|
+
postLines.push(` ${DIM}enter${RESET} submit ${DIM}^J/⌥⏎${RESET} newline${state.editorAvailable ? ` ${DIM}^O${RESET} editor` : ''} ${DIM}esc${RESET} cancel`);
|
|
208
220
|
}
|
|
209
221
|
else {
|
|
210
|
-
postLines.push(...renderActions(interaction, state.selectedAction, maxW, response));
|
|
222
|
+
postLines.push(...renderActions(interaction, state.selectedAction, maxW, response, state.followUp?.status !== 'running'));
|
|
211
223
|
}
|
|
212
224
|
// Transient hint (e.g. an empty multi-select Enter that was rejected). Sits
|
|
213
225
|
// just above the footer; cleared on the next keypress.
|
|
@@ -236,6 +248,7 @@ function buildItemReviewLayout(state, cols, rows) {
|
|
|
236
248
|
export function clampItemReviewScroll(state, cols, rows) {
|
|
237
249
|
const { maxScroll } = buildItemReviewLayout(state, cols, rows);
|
|
238
250
|
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset || 0, maxScroll));
|
|
251
|
+
state.bodyScrollOffsets[state.bodyMode] = state.scrollOffset;
|
|
239
252
|
}
|
|
240
253
|
export function renderItemReview(state, cols, rows) {
|
|
241
254
|
const { interaction, preLines, bodyLines, postLines, maxW, bodyHeight, maxScroll, overflows } = buildItemReviewLayout(state, cols, rows);
|
|
@@ -262,18 +275,22 @@ export function renderItemReview(state, cols, rows) {
|
|
|
262
275
|
`${DIM}n/p${RESET} prev/next`,
|
|
263
276
|
`${DIM}space${RESET} toggle`,
|
|
264
277
|
`${DIM}enter${RESET} confirm`,
|
|
278
|
+
`${DIM}shift-tab${RESET} ${state.bodyMode === 'question' ? 'visual' : 'question'}`,
|
|
265
279
|
`${DIM}q${RESET} overview`,
|
|
266
280
|
]
|
|
267
281
|
: [
|
|
268
282
|
`${DIM}n/p${RESET} prev/next`,
|
|
269
|
-
`${DIM}
|
|
283
|
+
`${DIM}shift-tab${RESET} ${state.bodyMode === 'question' ? 'visual' : 'question'}`,
|
|
270
284
|
`${DIM}q${RESET} overview`,
|
|
271
285
|
];
|
|
272
286
|
if (overflows) {
|
|
273
287
|
footerParts.unshift(state.inputMode ? `${DIM}pgup/pgdn${RESET} scroll` : `${DIM}u/d${RESET} scroll`);
|
|
274
288
|
}
|
|
275
|
-
if (state.inputMode === null)
|
|
289
|
+
if (state.inputMode === null) {
|
|
290
|
+
if (state.followUpAvailable)
|
|
291
|
+
footerParts.push(`${DIM}?${RESET} follow-up`);
|
|
276
292
|
footerParts.push(`${DIM}w${RESET} browser`);
|
|
293
|
+
}
|
|
277
294
|
const footer = ` ${footerParts.join(' ')}`;
|
|
278
295
|
// Assemble — pad to fill rows so post-body sits at the bottom
|
|
279
296
|
const lines = [...preLines, ...visibleBody, ...postLines];
|
|
@@ -288,7 +305,7 @@ export function renderItemReview(state, cols, rows) {
|
|
|
288
305
|
// whole block when the terminal is wider than that.
|
|
289
306
|
return centerHorizontal(clamped, cols, maxW + 2);
|
|
290
307
|
}
|
|
291
|
-
function renderActions(interaction, selectedAction, maxW, existing) {
|
|
308
|
+
function renderActions(interaction, selectedAction, maxW, existing, showFocus = true) {
|
|
292
309
|
const lines = [];
|
|
293
310
|
const opts = interaction.options;
|
|
294
311
|
// Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
|
|
@@ -301,7 +318,7 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
301
318
|
const optionComments = existing !== undefined ? existing.optionComments : undefined;
|
|
302
319
|
for (let i = 0; i < opts.length; i++) {
|
|
303
320
|
const o = opts[i];
|
|
304
|
-
const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
321
|
+
const cursor = showFocus && i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
305
322
|
const sc = o.shortcut === undefined ? ' ' : o.shortcut;
|
|
306
323
|
const keyBadge = `${DIM}[${sc}]${RESET}`;
|
|
307
324
|
const box = multi
|
|
@@ -329,7 +346,7 @@ function renderActions(interaction, selectedAction, maxW, existing) {
|
|
|
329
346
|
}
|
|
330
347
|
}
|
|
331
348
|
if (interaction.allowFreetext && opts.length > 0) {
|
|
332
|
-
const cursor = opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
349
|
+
const cursor = showFocus && opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
|
|
333
350
|
let label;
|
|
334
351
|
if (interaction.freetextLabel !== undefined)
|
|
335
352
|
label = interaction.freetextLabel;
|
package/dist/tui/terminal.d.ts
CHANGED
package/dist/tui/terminal.js
CHANGED
|
@@ -17,6 +17,7 @@ function emptyKey() {
|
|
|
17
17
|
ctrl: false,
|
|
18
18
|
meta: false,
|
|
19
19
|
tab: false,
|
|
20
|
+
backTab: false,
|
|
20
21
|
backspace: false,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -69,6 +70,10 @@ export function parseKeypress(data) {
|
|
|
69
70
|
key.del = true;
|
|
70
71
|
return { input: '', key };
|
|
71
72
|
}
|
|
73
|
+
if (str === '\x1b[Z') {
|
|
74
|
+
key.backTab = true;
|
|
75
|
+
return { input: '', key };
|
|
76
|
+
}
|
|
72
77
|
// Alt+Enter inserts a newline in freetext (distinct from Enter=submit). Must
|
|
73
78
|
// precede the bare-ESC and meta-backspace checks so the two-byte sequence
|
|
74
79
|
// isn't swallowed as a lone escape.
|
package/dist/tui/tmux.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { logPopupEvent } from './log.js';
|
|
7
8
|
const POPUP_TITLE = 'humanloop · inbox';
|
|
8
9
|
const POPUP_STYLE = 'bg=#20242d';
|
|
9
10
|
const POPUP_BORDER_STYLE = 'fg=#5c6370';
|
|
@@ -54,33 +55,42 @@ function acquireStartupLock(path) {
|
|
|
54
55
|
}
|
|
55
56
|
export async function toggleInboxPopup(target) {
|
|
56
57
|
const socket = target?.socket ?? tmuxSocketFromEnvironment();
|
|
57
|
-
if (socket === undefined)
|
|
58
|
+
if (socket === undefined) {
|
|
59
|
+
logPopupEvent('toggle.rejected', { reason: 'not_in_tmux' });
|
|
58
60
|
return 'not_in_tmux';
|
|
61
|
+
}
|
|
59
62
|
const inferred = target?.client ?? inferTmuxClient(socket, target?.targetPane);
|
|
60
|
-
if (inferred === 'ambiguous')
|
|
61
|
-
|
|
62
|
-
if (inferred === undefined)
|
|
63
|
+
if (inferred === 'ambiguous' || inferred === undefined) {
|
|
64
|
+
logPopupEvent('toggle.rejected', { reason: 'ambiguous_client', socket, targetPane: target?.targetPane });
|
|
63
65
|
return 'ambiguous_client';
|
|
66
|
+
}
|
|
64
67
|
const resolved = { socket, client: inferred, targetPane: target?.targetPane };
|
|
65
68
|
const paths = popupPaths(resolved);
|
|
69
|
+
logPopupEvent('toggle.requested', { ...resolved, controlSocket: paths.controlSocket });
|
|
66
70
|
mkdirSync(join(paths.controlSocket, '..'), { recursive: true, mode: 0o700 });
|
|
67
|
-
if (await requestPopupClose(paths.controlSocket))
|
|
71
|
+
if (await requestPopupClose(paths.controlSocket)) {
|
|
72
|
+
logPopupEvent('toggle.closed', { ...resolved, controlSocket: paths.controlSocket });
|
|
68
73
|
return 'closed';
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
if (!acquireStartupLock(paths.startupLock))
|
|
74
|
+
}
|
|
75
|
+
// A concurrent toggle for this same client owns popup startup. This gesture
|
|
76
|
+
// leaves that startup as the sole owner and reports a benign close result.
|
|
77
|
+
if (!acquireStartupLock(paths.startupLock)) {
|
|
78
|
+
logPopupEvent('toggle.coalesced', { ...resolved, controlSocket: paths.controlSocket });
|
|
73
79
|
return 'closed';
|
|
80
|
+
}
|
|
74
81
|
try {
|
|
75
|
-
// Re-probe under the lock
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
// Re-probe under the lock so a popup that became live during lock acquisition
|
|
83
|
+
// is closed through its controller rather than orphaned.
|
|
84
|
+
if (await requestPopupClose(paths.controlSocket)) {
|
|
85
|
+
logPopupEvent('toggle.closed', { ...resolved, controlSocket: paths.controlSocket, phase: 'locked-reprobe' });
|
|
79
86
|
return 'closed';
|
|
87
|
+
}
|
|
80
88
|
if (existsSync(paths.controlSocket))
|
|
81
89
|
rmSync(paths.controlSocket, { force: true });
|
|
82
90
|
const command = `${quote(process.execPath)} ${quote(fileURLToPath(new URL('../cli.js', import.meta.url)))} inbox open --control-socket ${quote(paths.controlSocket)}`;
|
|
83
|
-
|
|
91
|
+
const result = await launchPopup(socket, resolved, paths.controlSocket, command);
|
|
92
|
+
logPopupEvent('toggle.completed', { ...resolved, controlSocket: paths.controlSocket, result });
|
|
93
|
+
return result;
|
|
84
94
|
}
|
|
85
95
|
finally {
|
|
86
96
|
rmSync(paths.startupLock, { recursive: true, force: true });
|
|
@@ -100,17 +110,27 @@ async function requestPopupClose(controlSocket) {
|
|
|
100
110
|
/** Launch one popup and report `opened` only once its controller owns the control socket. */
|
|
101
111
|
async function launchPopup(socket, target, controlSocket, command) {
|
|
102
112
|
const args = ['-S', socket, 'display-popup', '-E', '-c', target.client, ...inboxPopupFlags(), ...(target.targetPane === undefined ? [] : ['-t', target.targetPane]), command];
|
|
113
|
+
const startedAt = Date.now();
|
|
103
114
|
const child = spawn('tmux', args, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
|
|
115
|
+
logPopupEvent('popup.spawned', { ...target, controlSocket, pid: child.pid, args });
|
|
104
116
|
let stderr = '';
|
|
105
|
-
child.stderr.on('data', (chunk) => {
|
|
117
|
+
child.stderr.on('data', (chunk) => {
|
|
118
|
+
stderr += chunk.toString('utf8');
|
|
119
|
+
if (stderr.length > 8192)
|
|
120
|
+
stderr = stderr.slice(-8192);
|
|
121
|
+
});
|
|
106
122
|
const exited = new Promise((resolve) => {
|
|
107
|
-
// `display-popup -E` blocks until
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
// `display-popup -E` blocks until the popup closes. An exit before the
|
|
124
|
+
// controller socket is live means tmux declined or failed the launch.
|
|
125
|
+
child.once('exit', (code, signal) => {
|
|
126
|
+
const result = code === 0 || /popup/i.test(stderr) ? 'other_popup' : 'failed';
|
|
127
|
+
logPopupEvent('popup.exited', { ...target, controlSocket, pid: child.pid, code, signal, result, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
|
|
128
|
+
resolve(result);
|
|
129
|
+
});
|
|
130
|
+
child.once('error', (error) => {
|
|
131
|
+
logPopupEvent('popup.error', { ...target, controlSocket, pid: child.pid, error: error.message, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
|
|
132
|
+
resolve('failed');
|
|
133
|
+
});
|
|
114
134
|
});
|
|
115
135
|
const { Socket } = await import('node:net');
|
|
116
136
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -125,12 +145,14 @@ async function launchPopup(socket, target, controlSocket, command) {
|
|
|
125
145
|
}),
|
|
126
146
|
]);
|
|
127
147
|
if (outcome === 'opened') {
|
|
148
|
+
logPopupEvent('popup.opened', { ...target, controlSocket, pid: child.pid, elapsedMs: Date.now() - startedAt });
|
|
128
149
|
child.unref();
|
|
129
150
|
return 'opened';
|
|
130
151
|
}
|
|
131
152
|
if (outcome !== 'pending')
|
|
132
153
|
return outcome;
|
|
133
154
|
}
|
|
155
|
+
logPopupEvent('popup.startup_timeout', { ...target, controlSocket, pid: child.pid, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
|
|
134
156
|
return 'failed';
|
|
135
157
|
}
|
|
136
158
|
/** The static tmux `display-popup` geometry and style flags; the client and target pane are added per-invocation. */
|
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface DeckSource {
|
|
|
68
68
|
* a crouter canvas node. Lets per-node attention scoping attribute the ask
|
|
69
69
|
* to the node that raised it rather than every sibling sharing the cwd. */
|
|
70
70
|
nodeId?: string;
|
|
71
|
+
/** Host-owned durable Visual capability marker. A registered handler is also required. */
|
|
72
|
+
visual?: 'humanloop.visual/v1';
|
|
71
73
|
}
|
|
72
74
|
export interface Deck {
|
|
73
75
|
title?: string;
|
|
@@ -103,8 +105,21 @@ export interface FeedbackResult {
|
|
|
103
105
|
export interface VisualBlock {
|
|
104
106
|
questionId: string;
|
|
105
107
|
content: string;
|
|
108
|
+
/** Original model output, retained so resize can re-render locally. */
|
|
109
|
+
markdown?: string;
|
|
106
110
|
status: 'loading' | 'ready' | 'error';
|
|
107
111
|
}
|
|
112
|
+
export type FollowUpState = {
|
|
113
|
+
status: 'idle';
|
|
114
|
+
} | {
|
|
115
|
+
status: 'running';
|
|
116
|
+
} | {
|
|
117
|
+
status: 'ready';
|
|
118
|
+
markdown: string;
|
|
119
|
+
} | {
|
|
120
|
+
status: 'error';
|
|
121
|
+
error: string;
|
|
122
|
+
};
|
|
108
123
|
export type Phase = 'overview' | 'item-review' | 'final';
|
|
109
124
|
export type InputMode = null | {
|
|
110
125
|
kind: 'comment';
|
|
@@ -115,6 +130,10 @@ export type InputMode = null | {
|
|
|
115
130
|
kind: 'freetext';
|
|
116
131
|
buffer: string;
|
|
117
132
|
cursor: number;
|
|
133
|
+
} | {
|
|
134
|
+
kind: 'follow-up';
|
|
135
|
+
buffer: string;
|
|
136
|
+
cursor: number;
|
|
118
137
|
};
|
|
119
138
|
export interface TuiState {
|
|
120
139
|
phase: Phase;
|
|
@@ -128,8 +147,16 @@ export interface TuiState {
|
|
|
128
147
|
preAnsweredIds: Set<string>;
|
|
129
148
|
inputMode: InputMode;
|
|
130
149
|
selectedAction: number;
|
|
131
|
-
|
|
150
|
+
bodyMode: 'question' | 'visual';
|
|
132
151
|
scrollOffset: number;
|
|
152
|
+
bodyScrollOffsets: {
|
|
153
|
+
question: number;
|
|
154
|
+
visual: number;
|
|
155
|
+
};
|
|
156
|
+
/** The mounting host provided an active Ctrl+O editor callback. */
|
|
157
|
+
editorAvailable: boolean;
|
|
158
|
+
followUpAvailable: boolean;
|
|
159
|
+
followUp?: FollowUpState;
|
|
133
160
|
/** Transient one-line notice shown in item-review (e.g. an empty multi-select
|
|
134
161
|
* Enter that was rejected). Cleared on the next keypress. */
|
|
135
162
|
hint?: string;
|
|
@@ -228,18 +255,52 @@ export interface DisplayOpts {
|
|
|
228
255
|
/** Pane budget per window before `'auto'` opens a new window. Default 3. */
|
|
229
256
|
maxPanes?: number;
|
|
230
257
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
/** Width-free Interaction snapshot shared by the panel and durable Visual protocol. */
|
|
259
|
+
export interface CanonicalInteractionOption {
|
|
260
|
+
id: string;
|
|
261
|
+
label: string;
|
|
262
|
+
description?: string;
|
|
263
|
+
}
|
|
264
|
+
export interface CanonicalInteraction {
|
|
265
|
+
id: string;
|
|
266
|
+
title: string;
|
|
267
|
+
subtitle?: string;
|
|
268
|
+
body?: string;
|
|
269
|
+
options: CanonicalInteractionOption[];
|
|
270
|
+
multiSelect?: boolean;
|
|
271
|
+
allowFreetext?: boolean;
|
|
272
|
+
freetextLabel?: string;
|
|
273
|
+
kind?: InteractionKind;
|
|
274
|
+
preAnswered?: InteractionPreAnswer;
|
|
275
|
+
}
|
|
276
|
+
export interface VisualRequest {
|
|
277
|
+
requestId: string;
|
|
278
|
+
generationId: string;
|
|
279
|
+
interaction: CanonicalInteraction;
|
|
280
|
+
}
|
|
281
|
+
export type VisualResult = {
|
|
282
|
+
status: 'ready';
|
|
234
283
|
markdown: string;
|
|
235
284
|
} | {
|
|
236
|
-
|
|
285
|
+
status: 'error';
|
|
237
286
|
error: string;
|
|
238
|
-
}
|
|
287
|
+
};
|
|
288
|
+
export interface VisualHandle {
|
|
289
|
+
result: Promise<VisualResult>;
|
|
290
|
+
/** Synchronous and idempotent. A canceled handle has no renderable result. */
|
|
291
|
+
cancel(): void;
|
|
292
|
+
}
|
|
293
|
+
/** Host-injected, Markdown-only Visual capability. Rendering width belongs to the panel. */
|
|
294
|
+
export type VisualProvider = (request: VisualRequest) => VisualHandle;
|
|
239
295
|
export interface MountedPanelOpts {
|
|
240
296
|
deck: Deck;
|
|
241
297
|
progressPath?: string;
|
|
242
|
-
|
|
298
|
+
visualProvider?: VisualProvider;
|
|
299
|
+
/** Host callback for Ctrl+O while a comment/freetext buffer is active. */
|
|
300
|
+
onEditorRequest?: () => void;
|
|
301
|
+
followUpAvailable?: boolean;
|
|
302
|
+
onFollowUpRequest?: (question: string) => void;
|
|
303
|
+
onFollowUpCancel?: () => void;
|
|
243
304
|
cols: number;
|
|
244
305
|
rows: number;
|
|
245
306
|
onProgress?: (responses: InteractionResponse[]) => void;
|
|
@@ -258,7 +319,10 @@ export interface MountedPanel {
|
|
|
258
319
|
unmount(): void;
|
|
259
320
|
loadDeck(deck: Deck, opts?: {
|
|
260
321
|
progressPath?: string;
|
|
322
|
+
visualProvider?: VisualProvider;
|
|
261
323
|
}): void;
|
|
324
|
+
setFollowUpHandlers(available: boolean, onRequest?: (question: string) => void, onCancel?: () => void): void;
|
|
325
|
+
setFollowUpState(state: FollowUpState): void;
|
|
262
326
|
canAcceptHostKeys(): boolean;
|
|
263
327
|
/**
|
|
264
328
|
* True when the deck is at its top level: overview phase with no active
|