@crouton-kit/humanloop 0.3.37 → 0.3.39
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/browser/server.d.ts +15 -5
- package/dist/browser/server.js +110 -14
- package/dist/cli.js +0 -0
- package/dist/conversation/reader.d.ts +15 -1
- package/dist/conversation/reader.js +320 -30
- 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 +36 -1
- package/dist/inbox/controller.js +335 -28
- package/dist/inbox/convention.d.ts +7 -0
- package/dist/inbox/convention.js +32 -0
- 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 +2 -0
- package/dist/inbox/registry.js +6 -2
- package/dist/inbox/tickets.js +8 -2
- package/dist/inbox/tui.d.ts +1 -1
- package/dist/inbox/tui.js +58 -25
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/render/termrender.js +25 -26
- package/dist/tui/app.d.ts +1 -1
- package/dist/tui/app.js +80 -88
- 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 +42 -23
- 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 +39 -2
- package/dist/visuals/conversation.d.ts +7 -0
- package/dist/visuals/conversation.js +16 -0
- package/dist/visuals/generate.d.ts +3 -1
- package/dist/visuals/generate.js +8 -6
- package/dist/web/assets/{index-DhbBiRqS.js → index-YFwgZZMg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +7 -2
package/dist/tui/render.js
CHANGED
|
@@ -74,7 +74,7 @@ export function renderOverview(state, cols, rows) {
|
|
|
74
74
|
lines.push('');
|
|
75
75
|
}
|
|
76
76
|
lines.push(` ${DIM}${hline(Math.min(cols - 4, 60))}${RESET}`);
|
|
77
|
-
lines.push(` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} finish`);
|
|
77
|
+
lines.push(` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}w${RESET} browser ${DIM}q${RESET} finish`);
|
|
78
78
|
while (lines.length < rows)
|
|
79
79
|
lines.push('');
|
|
80
80
|
// Overview content extends roughly cols-16 wide for option labels; center
|
|
@@ -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,7 +216,7 @@ 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
222
|
postLines.push(...renderActions(interaction, state.selectedAction, maxW, response));
|
|
@@ -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,16 +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
|
}
|
|
289
|
+
if (state.inputMode === null) {
|
|
290
|
+
if (state.followUpAvailable)
|
|
291
|
+
footerParts.push(`${DIM}?${RESET} follow-up`);
|
|
292
|
+
footerParts.push(`${DIM}w${RESET} browser`);
|
|
293
|
+
}
|
|
275
294
|
const footer = ` ${footerParts.join(' ')}`;
|
|
276
295
|
// Assemble — pad to fill rows so post-body sits at the bottom
|
|
277
296
|
const lines = [...preLines, ...visibleBody, ...postLines];
|
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,9 @@ 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
|
+
/** Durable identifier of the conversation session that originated this ask.
|
|
72
|
+
* It is explicit metadata, never inferred from node identity or labels. */
|
|
73
|
+
originatingConversationSessionId?: string;
|
|
71
74
|
}
|
|
72
75
|
export interface Deck {
|
|
73
76
|
title?: string;
|
|
@@ -103,8 +106,21 @@ export interface FeedbackResult {
|
|
|
103
106
|
export interface VisualBlock {
|
|
104
107
|
questionId: string;
|
|
105
108
|
content: string;
|
|
109
|
+
/** Original model output, retained so resize can re-render locally. */
|
|
110
|
+
markdown?: string;
|
|
106
111
|
status: 'loading' | 'ready' | 'error';
|
|
107
112
|
}
|
|
113
|
+
export type FollowUpState = {
|
|
114
|
+
status: 'idle';
|
|
115
|
+
} | {
|
|
116
|
+
status: 'running';
|
|
117
|
+
} | {
|
|
118
|
+
status: 'ready';
|
|
119
|
+
markdown: string;
|
|
120
|
+
} | {
|
|
121
|
+
status: 'error';
|
|
122
|
+
error: string;
|
|
123
|
+
};
|
|
108
124
|
export type Phase = 'overview' | 'item-review' | 'final';
|
|
109
125
|
export type InputMode = null | {
|
|
110
126
|
kind: 'comment';
|
|
@@ -115,6 +131,10 @@ export type InputMode = null | {
|
|
|
115
131
|
kind: 'freetext';
|
|
116
132
|
buffer: string;
|
|
117
133
|
cursor: number;
|
|
134
|
+
} | {
|
|
135
|
+
kind: 'follow-up';
|
|
136
|
+
buffer: string;
|
|
137
|
+
cursor: number;
|
|
118
138
|
};
|
|
119
139
|
export interface TuiState {
|
|
120
140
|
phase: Phase;
|
|
@@ -128,8 +148,16 @@ export interface TuiState {
|
|
|
128
148
|
preAnsweredIds: Set<string>;
|
|
129
149
|
inputMode: InputMode;
|
|
130
150
|
selectedAction: number;
|
|
131
|
-
|
|
151
|
+
bodyMode: 'question' | 'visual';
|
|
132
152
|
scrollOffset: number;
|
|
153
|
+
bodyScrollOffsets: {
|
|
154
|
+
question: number;
|
|
155
|
+
visual: number;
|
|
156
|
+
};
|
|
157
|
+
/** The mounting host provided an active Ctrl+O editor callback. */
|
|
158
|
+
editorAvailable: boolean;
|
|
159
|
+
followUpAvailable: boolean;
|
|
160
|
+
followUp?: FollowUpState;
|
|
133
161
|
/** Transient one-line notice shown in item-review (e.g. an empty multi-select
|
|
134
162
|
* Enter that was rejected). Cleared on the next keypress. */
|
|
135
163
|
hint?: string;
|
|
@@ -228,7 +256,9 @@ export interface DisplayOpts {
|
|
|
228
256
|
/** Pane budget per window before `'auto'` opens a new window. Default 3. */
|
|
229
257
|
maxPanes?: number;
|
|
230
258
|
}
|
|
231
|
-
export type GenerateVisual = (interaction: Interaction
|
|
259
|
+
export type GenerateVisual = (interaction: Interaction,
|
|
260
|
+
/** Render width owned by the mounting surface. */
|
|
261
|
+
cols: number) => Promise<{
|
|
232
262
|
ok: true;
|
|
233
263
|
ansi: string;
|
|
234
264
|
markdown: string;
|
|
@@ -240,6 +270,11 @@ export interface MountedPanelOpts {
|
|
|
240
270
|
deck: Deck;
|
|
241
271
|
progressPath?: string;
|
|
242
272
|
generateVisual?: GenerateVisual;
|
|
273
|
+
/** Host callback for Ctrl+O while a comment/freetext buffer is active. */
|
|
274
|
+
onEditorRequest?: () => void;
|
|
275
|
+
followUpAvailable?: boolean;
|
|
276
|
+
onFollowUpRequest?: (question: string) => void;
|
|
277
|
+
onFollowUpCancel?: () => void;
|
|
243
278
|
cols: number;
|
|
244
279
|
rows: number;
|
|
245
280
|
onProgress?: (responses: InteractionResponse[]) => void;
|
|
@@ -259,6 +294,8 @@ export interface MountedPanel {
|
|
|
259
294
|
loadDeck(deck: Deck, opts?: {
|
|
260
295
|
progressPath?: string;
|
|
261
296
|
}): void;
|
|
297
|
+
setFollowUpHandlers(available: boolean, onRequest?: (question: string) => void, onCancel?: () => void): void;
|
|
298
|
+
setFollowUpState(state: FollowUpState): void;
|
|
262
299
|
canAcceptHostKeys(): boolean;
|
|
263
300
|
/**
|
|
264
301
|
* True when the deck is at its top level: overview phase with no active
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenerateVisual, Interaction } from '../types.js';
|
|
2
|
+
import { defaultGenerateVisual } from './generate.js';
|
|
3
|
+
type VisualGenerator = (interaction: Interaction, conversationContext: string, cols?: number) => ReturnType<typeof defaultGenerateVisual>;
|
|
4
|
+
type ConversationTextReader = (sessionId: string) => Promise<string>;
|
|
5
|
+
/** Build the standard visual generator from explicit originating-session metadata. */
|
|
6
|
+
export declare function visualGeneratorForConversationSession(sessionId: string, generateVisual?: VisualGenerator, readContext?: ConversationTextReader): GenerateVisual;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readConversationText } from '../conversation/reader.js';
|
|
2
|
+
import { defaultGenerateVisual } from './generate.js';
|
|
3
|
+
/** Build the standard visual generator from explicit originating-session metadata. */
|
|
4
|
+
export function visualGeneratorForConversationSession(sessionId, generateVisual = defaultGenerateVisual, readContext = readConversationText) {
|
|
5
|
+
// One lookup feeds every interaction and resize regeneration in this mounted panel.
|
|
6
|
+
// A failed lookup remains a failed visual rather than falling through to a generic prompt.
|
|
7
|
+
const context = readContext(sessionId);
|
|
8
|
+
return async (interaction, cols) => {
|
|
9
|
+
try {
|
|
10
|
+
return await generateVisual(interaction, await context, cols);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { ok: false, error: 'visual context unavailable' };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Interaction } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/** Width shared by model-time and local resize-time visual rendering. */
|
|
3
|
+
export declare function visualRenderWidth(cols: number): number;
|
|
4
|
+
export declare function defaultGenerateVisual(interaction: Interaction, conversationContext: string, cols?: number): Promise<{
|
|
3
5
|
ok: true;
|
|
4
6
|
ansi: string;
|
|
5
7
|
markdown: string;
|
package/dist/visuals/generate.js
CHANGED
|
@@ -42,16 +42,18 @@ async function callHaiku(prompt, systemPrompt) {
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// embed humanloop in a sub-region should supply their own closure that bakes
|
|
48
|
-
// in the correct width.
|
|
45
|
+
// The mounting surface supplies its actual render width; generated ANSI must
|
|
46
|
+
// fit an embedded panel just as it fits a standalone terminal.
|
|
49
47
|
// Cap on how much conversation we hand the generator. Recency is what grounds
|
|
50
48
|
// the decision in front of the human, so when history is long we keep the tail
|
|
51
49
|
// (most recent messages) rather than the head.
|
|
52
50
|
const MAX_CONTEXT_CHARS = 24000;
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/** Width shared by model-time and local resize-time visual rendering. */
|
|
52
|
+
export function visualRenderWidth(cols) {
|
|
53
|
+
return Math.max(1, Math.min(cols - 4, 76));
|
|
54
|
+
}
|
|
55
|
+
export async function defaultGenerateVisual(interaction, conversationContext, cols = (process.stdout.columns || 80)) {
|
|
56
|
+
const width = visualRenderWidth(cols);
|
|
55
57
|
const optionsSummary = interaction.options.length > 0
|
|
56
58
|
? `\nOptions: ${interaction.options.map((o) => o.label).join(' | ')}`
|
|
57
59
|
: '';
|