@parallel-cli/parallel 0.4.8 → 0.5.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.
@@ -0,0 +1,93 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const PRIVATE_DIR = 0o700;
4
+ const PRIVATE_FILE = 0o600;
5
+ export function ensurePrivateDir(dir) {
6
+ fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
7
+ try {
8
+ fs.chmodSync(dir, PRIVATE_DIR);
9
+ }
10
+ catch {
11
+ // Best effort: some filesystems do not support chmod.
12
+ }
13
+ }
14
+ export function chmodPrivateFile(file) {
15
+ try {
16
+ fs.chmodSync(file, PRIVATE_FILE);
17
+ }
18
+ catch {
19
+ // Best effort only.
20
+ }
21
+ }
22
+ export function chmodPrivateTree(root) {
23
+ if (!fs.existsSync(root))
24
+ return;
25
+ const stat = fs.statSync(root);
26
+ if (stat.isDirectory()) {
27
+ try {
28
+ fs.chmodSync(root, PRIVATE_DIR);
29
+ }
30
+ catch { }
31
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
32
+ chmodPrivateTree(path.join(root, entry.name));
33
+ }
34
+ return;
35
+ }
36
+ if (stat.isFile())
37
+ chmodPrivateFile(root);
38
+ }
39
+ export function writeFileAtomicPrivate(file, content) {
40
+ ensurePrivateDir(path.dirname(file));
41
+ const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
42
+ let fd;
43
+ try {
44
+ fd = fs.openSync(tmp, 'w', PRIVATE_FILE);
45
+ fs.writeFileSync(fd, content, 'utf8');
46
+ fs.fsyncSync(fd);
47
+ fs.closeSync(fd);
48
+ fd = undefined;
49
+ fs.renameSync(tmp, file);
50
+ chmodPrivateFile(file);
51
+ }
52
+ finally {
53
+ if (fd !== undefined) {
54
+ try {
55
+ fs.closeSync(fd);
56
+ }
57
+ catch { }
58
+ }
59
+ try {
60
+ if (fs.existsSync(tmp))
61
+ fs.unlinkSync(tmp);
62
+ }
63
+ catch { }
64
+ }
65
+ }
66
+ export function appendFilePrivate(file, content) {
67
+ ensurePrivateDir(path.dirname(file));
68
+ fs.appendFileSync(file, content, { encoding: 'utf8', mode: PRIVATE_FILE });
69
+ chmodPrivateFile(file);
70
+ }
71
+ export function writeJsonAtomicPrivate(file, value) {
72
+ writeFileAtomicPrivate(file, JSON.stringify(value, null, 2));
73
+ }
74
+ export function sanitizeTerminalText(text) {
75
+ return text
76
+ // OSC sequences, including hyperlinks/window-title changes.
77
+ .replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, '')
78
+ // CSI sequences.
79
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
80
+ // Other one-byte ESC sequences.
81
+ .replace(/\x1B[@-Z\\-_]/g, '')
82
+ // C0 controls except tab/newline/carriage return.
83
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
84
+ }
85
+ export function redactPersistedText(text) {
86
+ return text
87
+ .replace(/data:image\/png;base64,[A-Za-z0-9+/=]+/g, 'data:image/png;base64,[redacted]')
88
+ .replace(/([A-Za-z0-9_]*API[_-]?KEY[A-Za-z0-9_]*\s*[:=]\s*)['"]?[A-Za-z0-9._~+/=-]{12,}['"]?/gi, '$1[redacted]')
89
+ .replace(/(sk-[A-Za-z0-9]{16,})/g, '[redacted-api-key]');
90
+ }
91
+ export function sanitizeForPersistence(text) {
92
+ return redactPersistedText(sanitizeTerminalText(text));
93
+ }
package/dist/server.js CHANGED
@@ -1,17 +1,33 @@
1
1
  import net from 'node:net';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { ensurePrivateDir, writeFileAtomicPrivate } from './security.js';
4
6
  export function socketPath(projectRoot) {
5
7
  return path.join(projectRoot, '.parallel', 'session.sock');
6
8
  }
9
+ export function sessionTokenPath(projectRoot) {
10
+ return path.join(projectRoot, '.parallel', 'session.token');
11
+ }
12
+ export function readSessionToken(projectRoot) {
13
+ try {
14
+ return fs.readFileSync(sessionTokenPath(projectRoot), 'utf8').trim() || null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
7
20
  /** Start the session server. Returns a stop function (closes socket + clients). */
8
21
  export function startSessionServer(ctl) {
9
22
  const sock = socketPath(ctl.projectRoot);
23
+ const tokenFile = sessionTokenPath(ctl.projectRoot);
24
+ const token = randomBytes(32).toString('hex');
10
25
  try {
11
- fs.mkdirSync(path.dirname(sock), { recursive: true });
26
+ ensurePrivateDir(path.dirname(sock));
12
27
  // A previous run may have crashed without cleaning up: remove the stale socket.
13
28
  if (fs.existsSync(sock))
14
29
  fs.unlinkSync(sock);
30
+ writeFileAtomicPrivate(tokenFile, token);
15
31
  }
16
32
  catch {
17
33
  return null;
@@ -69,7 +85,7 @@ export function startSessionServer(ctl) {
69
85
  };
70
86
  ctl.on('update', onUpdate);
71
87
  const server = net.createServer((socket) => {
72
- const client = { socket, agent: '', lastSeq: 0 };
88
+ const client = { socket, agent: '', lastSeq: 0, authenticated: false };
73
89
  let buffer = '';
74
90
  socket.setEncoding('utf8');
75
91
  socket.on('data', (chunk) => {
@@ -88,10 +104,19 @@ export function startSessionServer(ctl) {
88
104
  continue;
89
105
  }
90
106
  if (msg.type === 'hello' && typeof msg.agent === 'string') {
107
+ if (msg.token !== token) {
108
+ send(socket, { type: 'bye' });
109
+ socket.destroy();
110
+ continue;
111
+ }
112
+ client.authenticated = true;
91
113
  client.agent = msg.agent;
92
114
  clients.add(client);
93
115
  pushTo(client); // immediate first snapshot (full backlog: lastSeq = 0)
94
116
  }
117
+ else if (!client.authenticated) {
118
+ continue;
119
+ }
95
120
  else if (msg.type === 'input' && typeof msg.text === 'string' && client.agent) {
96
121
  const text = msg.text.trim();
97
122
  if (!text)
@@ -148,6 +173,14 @@ export function startSessionServer(ctl) {
148
173
  catch {
149
174
  return null;
150
175
  }
176
+ server.on('listening', () => {
177
+ try {
178
+ fs.chmodSync(sock, 0o600);
179
+ }
180
+ catch {
181
+ /* best effort */
182
+ }
183
+ });
151
184
  server.on('error', () => {
152
185
  /* keep the TUI alive even if the server dies */
153
186
  });
@@ -165,5 +198,11 @@ export function startSessionServer(ctl) {
165
198
  catch {
166
199
  /* already gone */
167
200
  }
201
+ try {
202
+ fs.unlinkSync(tokenFile);
203
+ }
204
+ catch {
205
+ /* already gone */
206
+ }
168
207
  };
169
208
  }
@@ -13,6 +13,7 @@ export const KIND_COLOR = {
13
13
  llm: UI.muted,
14
14
  error: UI.danger,
15
15
  note: UI.note,
16
+ memory: COLOR.creamMuted,
16
17
  system: UI.warn,
17
18
  info: UI.text,
18
19
  };
@@ -29,8 +30,11 @@ export function cleanHubSummary(text) {
29
30
  export function formatAgentTelemetry(agent) {
30
31
  const ctx = agent.ctxPct !== undefined ? ` · ${agent.ctxPct}% ctx` : '';
31
32
  const perf = agent.perf ? ` · ${agent.perf.modelTurns}t/${agent.perf.toolCalls} tools` : '';
33
+ const llm = agent.perf?.llmMs ? ` · llm ${Math.round(agent.perf.llmMs / 1000)}s` : '';
32
34
  const runtime = agent.endedAt ? `ended ${elapsed(agent.startedAt, agent.endedAt)}` : elapsed(agent.startedAt);
33
- return `${runtime}${ctx}${perf} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
35
+ const cache = agent.perf?.cachedTokens ? ` · cache ${Math.round(agent.perf.cachedTokens / 1000)}k` : '';
36
+ const profile = agent.profile ? ` · ${agent.profile}` : '';
37
+ return `${runtime}${profile}${ctx}${perf}${llm}${cache} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
34
38
  }
35
39
  function firstSectionLine(text, labels) {
36
40
  const lines = text.replace(/\r/g, '').split('\n');
@@ -166,7 +170,7 @@ export function AgentRow({ agent, logs, cols, }) {
166
170
  else if (claims) {
167
171
  line2 = { text: claims, color: UI.warn };
168
172
  }
169
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), _jsxs(Text, { color: mode.color, children: [" [", mode.label, "]"] }), specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsx(Text, { color: UI.muted, wrap: "truncate-end", children: truncate(quickActions, actionBudget) })] }), summary.length > 0 ? (_jsx(Box, { flexDirection: "column", children: summary.map((line, i) => (_jsxs(Box, { flexDirection: "row", justifyContent: i === 0 ? 'space-between' : undefined, children: [_jsxs(Text, { color: COLOR.cream, wrap: "truncate-end", children: [_jsx(Text, { color: COLOR.cream, children: "\u2022 " }), line] }), i === 0 ? _jsx(Text, { color: UI.muted, children: telemetry }) : null] }, `${i}-${line}`))) })) : line2 ? (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })) : null, !agent.lastResult ? _jsx(ProgressSteps, { agent: agent, max: 3, cols: line2Max, showRemaining: true }) : null] }));
173
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), _jsxs(Text, { color: mode.color, children: [" [", mode.label, "]"] }), agent.profile ? _jsxs(Text, { color: UI.muted, children: [" [", agent.profile.toUpperCase(), "]"] }) : null, specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsx(Text, { color: UI.muted, wrap: "truncate-end", children: truncate(quickActions, actionBudget) })] }), summary.length > 0 ? (_jsx(Box, { flexDirection: "column", children: summary.map((line, i) => (_jsxs(Box, { flexDirection: "row", justifyContent: i === 0 ? 'space-between' : undefined, children: [_jsxs(Text, { color: COLOR.cream, wrap: "truncate-end", children: [_jsx(Text, { color: COLOR.cream, children: "\u2022 " }), line] }), i === 0 ? _jsx(Text, { color: UI.muted, children: telemetry }) : null] }, `${i}-${line}`))) })) : line2 ? (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })) : null, !agent.lastResult ? _jsx(ProgressSteps, { agent: agent, max: 3, cols: line2Max, showRemaining: true }) : null] }));
170
174
  }
171
175
  export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, cols = 100, }) {
172
176
  const meta = STATE_META[agent.state];
package/dist/ui/App.js CHANGED
@@ -644,7 +644,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
644
644
  specialists: 'specialists',
645
645
  };
646
646
  const viewLabel = VIEW_LABEL[view] ?? 'control room';
647
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: headerColor, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", agents.length === 0 ? 'ready' : viewLabel] }), rawLogs && focused ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] }), workMapAlerts.length > 0 ? (_jsxs(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: [" \u00B7 \u26A0 work-map ", workMapAlerts.length] })) : null] }) })) : (_jsx(Text, { color: CHROME.muted, children: providerModel })), _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Text, { color: headerColor, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight, onSelect: (cmd) => onInput(cmd) })) : agents.length === 0 ? (_jsx(EmptyHub, { bodyHeight: bodyHeight })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll, cols: cols }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
647
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: headerColor, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", agents.length === 0 ? 'ready' : viewLabel] }), rawLogs && focused ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] }), workMapAlerts.length > 0 ? (_jsxs(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: [" \u00B7 \u26A0 work-map ", workMapAlerts.length] })) : null] }) })) : (_jsx(Text, { color: CHROME.muted, children: providerModel })), _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Text, { color: headerColor, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { ctl: ctl, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight, onSelect: (cmd) => onInput(cmd) })) : agents.length === 0 ? (_jsx(EmptyHub, { bodyHeight: bodyHeight })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll, cols: cols }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
648
648
  ? systemLines
649
649
  .filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
650
650
  .slice(-2)
@@ -658,7 +658,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
658
658
  return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
659
659
  })] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(Text, { children: " " }), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : t('main.prompt'), context: focus ? 'focus' : 'hub', targetAgent: focused?.name, modelLabel: ctl.sessionProvider() ? `${ctl.sessionProvider()?.name}:${ctl.session.model}` : undefined, agentNames: agentNames, agents: agents, width: cols, onHeightChange: setInputRows, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { children: " " }), _jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "/ for commands" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
660
660
  ctl.session.approvalMode === 'yolo' ? UI.danger :
661
- UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), agents.length > 0 ? _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions ", Controller.listSessions(ctl.projectRoot).length] }) : null, ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, workMapAlerts.length > 0 ? (_jsx(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: " \u00B7 \u26A0 /board" })) : null, conflictAlerts.length > 0 ? (_jsx(Text, { color: UI.danger, children: " \u00B7 run /review all" })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
661
+ UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), agents.length > 0 ? _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions ", Controller.listSessions(ctl.projectRoot).length] }) : null, _jsxs(Text, { color: ctl.projectContextStatus().status === 'ready' ? UI.ok :
662
+ ctl.projectContextStatus().status === 'indexing' ? UI.warn :
663
+ CHROME.muted, children: [" \u00B7 ", t('memory.label'), " ", ctl.projectContextStatus().status] }), _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 index ", ctl.projectIndexStatus().files] }), ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, workMapAlerts.length > 0 ? (_jsx(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: " \u00B7 \u26A0 /board" })) : null, conflictAlerts.length > 0 ? (_jsx(Text, { color: UI.danger, children: " \u00B7 run /review all" })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
662
664
  }
663
665
  function groupAgents(agents) {
664
666
  const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
@@ -60,6 +60,9 @@ function AttachStaticLine({ item, raw }) {
60
60
  const event = toUIEvents([item.log])[0];
61
61
  if (!event || event.kind === 'thought')
62
62
  return _jsx(Text, { color: UI.muted, children: " " });
63
+ if (event.kind === 'memory') {
64
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: UI.muted, paddingX: 1, children: _jsxs(Text, { color: COLOR.creamMuted, wrap: "wrap", children: [_jsx(Text, { bold: true, children: "o " }), truncate(event.detail, process.stdout.columns ? process.stdout.columns - 8 : 120)] }) }));
65
+ }
63
66
  const color = event.kind === 'error' ? UI.danger : event.kind === 'note' ? UI.note : event.kind === 'command' ? UI.accent : UI.muted;
64
67
  const detail = event.detail.replace(/\r/g, '').split('\n').filter(Boolean).slice(0, 3).join(' ↳ ');
65
68
  return (_jsxs(Text, { color: color, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsx(Text, { bold: true, children: event.label }), detail ? _jsxs(Text, { color: event.kind === 'command_output' ? UI.muted : color, children: [" ", truncate(detail, process.stdout.columns ? process.stdout.columns - 8 : 120)] }) : null] }));
@@ -76,7 +79,7 @@ function AttachResultCard({ item }) {
76
79
  const st = STATE_META[item.info.state];
77
80
  return (_jsxs(Box, { borderStyle: "single", borderColor: st.color, flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: COLOR.cream, bold: true, children: ["Result \u00B7 ", item.info.name, " [", st.label, "]"] }), _jsx(Md, { text: item.result })] }));
78
81
  }
79
- export function AttachApp({ agentRef, sock }) {
82
+ export function AttachApp({ agentRef, sock, token }) {
80
83
  const { exit } = useApp();
81
84
  const { stdout } = useStdout();
82
85
  const [info, setInfo] = useState(null);
@@ -101,7 +104,7 @@ export function AttachApp({ agentRef, sock }) {
101
104
  let buffer = '';
102
105
  socket.setEncoding('utf8');
103
106
  socket.on('connect', () => {
104
- socket.write(JSON.stringify({ type: 'hello', agent: agentRef }) + '\n');
107
+ socket.write(JSON.stringify({ type: 'hello', agent: agentRef, token }) + '\n');
105
108
  });
106
109
  socket.on('data', (chunk) => {
107
110
  buffer += chunk;
@@ -138,7 +141,7 @@ export function AttachApp({ agentRef, sock }) {
138
141
  return () => {
139
142
  socket.destroy();
140
143
  };
141
- }, [agentRef, sock]);
144
+ }, [agentRef, sock, token]);
142
145
  useEffect(() => {
143
146
  if (!info || launchRendered.current)
144
147
  return;
@@ -89,6 +89,8 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
89
89
  const [selectedSuggestion, setSelectedSuggestion] = useState(0);
90
90
  const [cursorOn, setCursorOn] = useState(true);
91
91
  const attSeq = useRef(0);
92
+ const imageConsentUntil = useRef(0);
93
+ const imageConsentGranted = useRef(false);
92
94
  const reset = () => {
93
95
  setValue('');
94
96
  setAttachments([]);
@@ -127,6 +129,13 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
127
129
  notify?.(t('input.imageNone'));
128
130
  return;
129
131
  }
132
+ const now = Date.now();
133
+ if (!imageConsentGranted.current && imageConsentUntil.current < now) {
134
+ imageConsentUntil.current = now + 10_000;
135
+ notify?.(t('input.imageConsent'));
136
+ return;
137
+ }
138
+ imageConsentGranted.current = true;
130
139
  const n = ++attSeq.current;
131
140
  setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
132
141
  notify?.(t('input.imageAdded'));
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useCallback, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { createSkillTemplate, createSpecialistTemplate } from '../skills.js';
5
5
  import { priceFor } from '../pricing.js';
@@ -7,6 +7,9 @@ import { SelectList as BaseSelectList } from './Wizard.js';
7
7
  import { LANGS, getLang, setLang, t } from '../i18n.js';
8
8
  import { detectProviderModels, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
9
9
  import { BRAND } from './tokens.js';
10
+ function SettingsSelectList({ defaultBack, onBack, ...rest }) {
11
+ return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? defaultBack });
12
+ }
10
13
  function masked(key) {
11
14
  if (!key)
12
15
  return '—';
@@ -40,16 +43,12 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
40
43
  const saved = () => setFlash(t('set.saved'));
41
44
  const cfg = ctl.config;
42
45
  const listHeight = height ? Math.max(3, height - 5) : undefined;
43
- const goBack = () => {
46
+ const goBack = useCallback(() => {
44
47
  if (step.id === 'root')
45
48
  return onClose();
46
49
  setStep(returnStep ?? { id: 'root' });
47
50
  setReturnStep(null);
48
- };
49
- const SelectList = (props) => {
50
- const { onBack, ...rest } = props;
51
- return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? goBack });
52
- };
51
+ }, [onClose, returnStep, step.id]);
53
52
  // ---- root menu items ----
54
53
  const rootItems = scope === 'global'
55
54
  ? [
@@ -164,12 +163,12 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
164
163
  setStep(next);
165
164
  };
166
165
  // ---- render ----
167
- return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
166
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SettingsSelectList, { defaultBack: goBack, items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SettingsSelectList, { defaultBack: goBack, items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
168
167
  setLang(code);
169
168
  ctl.setLanguage(code);
170
169
  saved();
171
170
  setStep({ id: 'root' });
172
- } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SelectList, { items: [
171
+ } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
173
172
  ...cfg.providers.map((p) => ({ label: p.name, value: p.name, hint: `(${p.baseUrl})` })),
174
173
  { label: t('set.back'), value: '__back__' },
175
174
  ], height: listHeight, onSelect: (v) => {
@@ -183,7 +182,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
183
182
  if (step.next === 'models')
184
183
  return setStep({ id: 'modelList', provider: p });
185
184
  setStep({ id: 'model', provider: p });
186
- } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
185
+ } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
187
186
  ...step.provider.models.map((m) => ({
188
187
  label: m,
189
188
  value: m,
@@ -197,7 +196,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
197
196
  return;
198
197
  }
199
198
  pickModel(step.provider, v);
200
- }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'endpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.title', { name: step.provider.name }) }), _jsx(Text, { color: "gray", children: step.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: step.provider.defaultModel || step.provider.models[0] || '—' }) }), _jsx(SelectList, { items: [
199
+ }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'endpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.title', { name: step.provider.name }) }), _jsx(Text, { color: "gray", children: step.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: step.provider.defaultModel || step.provider.models[0] || '—' }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
201
200
  { label: t('wiz.provider.endpoint.use'), value: 'use' },
202
201
  { label: t('wiz.provider.endpoint.edit'), value: 'edit' },
203
202
  { label: t('set.back'), value: '__back__' },
@@ -220,7 +219,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
220
219
  saved();
221
220
  setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
222
221
  setReturnStep(null);
223
- } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
222
+ } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
224
223
  { label: t('set.setupScope.session'), value: 'session' },
225
224
  { label: t('set.setupScope.global'), value: 'global' },
226
225
  { label: t('set.back'), value: '__back__' },
@@ -228,10 +227,10 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
228
227
  if (v === '__back__')
229
228
  return setStep({ id: 'endpoint', provider: step.provider, setup: true });
230
229
  finishProviderSetup(step.provider, v === 'global');
231
- } })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
230
+ } })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
232
231
  const provider = { ...step.provider, baseUrl: url.trim() };
233
232
  setStep({ id: 'endpoint', provider, setup: step.setup });
234
- } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
233
+ } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
235
234
  ...step.provider.models.map((m) => ({
236
235
  label: m,
237
236
  value: m,
@@ -262,7 +261,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
262
261
  saved();
263
262
  setStep(returnStep ?? { id: 'root' });
264
263
  setReturnStep(null);
265
- } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
264
+ } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
266
265
  ...step.provider.models.map((m) => {
267
266
  const pr = priceFor(step.provider, m);
268
267
  return {
@@ -281,7 +280,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
281
280
  return;
282
281
  }
283
282
  goSub({ id: 'priceValue', provider: step.provider, model: v });
284
- }, onInput: (m) => goSub({ id: 'priceValue', provider: step.provider, model: m.trim() }) })] })), step.id === 'priceValue' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceValue', { model: step.model }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
283
+ }, onInput: (m) => goSub({ id: 'priceValue', provider: step.provider, model: m.trim() }) })] })), step.id === 'priceValue' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceValue', { model: step.model }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
285
284
  const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
286
285
  if (!m)
287
286
  return setFlash(t('set.priceBad'));
@@ -293,7 +292,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
293
292
  saved();
294
293
  setStep(returnStep ?? { id: 'root' });
295
294
  setReturnStep(null);
296
- } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
295
+ } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
297
296
  const provider = { ...step.provider, apiKey: k.trim() };
298
297
  if (step.setup) {
299
298
  if (scope === 'session') {
@@ -309,7 +308,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
309
308
  }
310
309
  setStep(returnStep ?? { id: 'root' });
311
310
  setReturnStep(null);
312
- } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => {
311
+ } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => {
313
312
  const trimmed = model.trim();
314
313
  if (isPlaceholderModel(trimmed)) {
315
314
  setFlash(t('set.modelPlaceholder'));
@@ -328,7 +327,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
328
327
  requiresApiKey: !local,
329
328
  },
330
329
  });
331
- } })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
330
+ } })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
332
331
  try {
333
332
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
334
333
  setFlash(t('m.skillCreated', { file }));
@@ -337,7 +336,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
337
336
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
338
337
  }
339
338
  setStep({ id: 'root' });
340
- } })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
339
+ } })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
341
340
  try {
342
341
  const file = createSpecialistTemplate(name.trim(), '', 'global', ctl.projectRoot);
343
342
  setFlash(t('m.specCreated', { file }));
@@ -346,7 +345,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
346
345
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
347
346
  }
348
347
  setStep({ id: 'root' });
349
- } })] })), step.id === 'providers' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: step.scope === 'global' ? t('set.providers.title') : t('sset.providers.title') }), _jsx(SelectList, { items: (() => {
348
+ } })] })), step.id === 'providers' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: step.scope === 'global' ? t('set.providers.title') : t('sset.providers.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: (() => {
350
349
  const configuredNames = new Set(cfg.providers.map((p) => p.name.toLowerCase()));
351
350
  const items = [];
352
351
  // Section: Configured
@@ -443,7 +442,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
443
442
  else {
444
443
  setStep({ id: 'providerDetail', provider: p, scope: 'global' });
445
444
  }
446
- } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
445
+ } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
447
446
  {
448
447
  label: t('set.providerDetail.key'),
449
448
  value: 'key',
@@ -508,7 +507,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
508
507
  }
509
508
  if (v === 'remove')
510
509
  return setStep({ id: 'removeProvider', provider: step.provider, scope: step.scope });
511
- } })] })), step.id === 'removeProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.removeProvider.title', { name: step.provider.name }) }), _jsx(Text, { color: "yellow", children: t('set.removeProvider.confirm') }), _jsx(SelectList, { items: [
510
+ } })] })), step.id === 'removeProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.removeProvider.title', { name: step.provider.name }) }), _jsx(Text, { color: "yellow", children: t('set.removeProvider.confirm') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
512
511
  { label: t('set.removeProvider.yes'), value: 'yes' },
513
512
  { label: t('set.removeProvider.no'), value: 'no' },
514
513
  ], height: listHeight, onSelect: (v) => {
@@ -42,6 +42,9 @@ function TimelineRow({ item, cols }) {
42
42
  if (item.kind === 'narration') {
43
43
  return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
44
44
  }
45
+ if (item.label === 'memory') {
46
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: UI.muted, paddingX: 1, children: _jsxs(Text, { color: UI.muted, wrap: "wrap", children: [_jsx(Text, { bold: true, children: "o " }), truncate(item.detail ?? '', max)] }) }));
47
+ }
45
48
  if (item.kind === 'command') {
46
49
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '', max) })] }), _jsx(OutputLines, { item: item, cols: cols })] }));
47
50
  }