@rind-ai/cli 0.6.1 → 0.7.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.
@@ -18,17 +18,13 @@ import { ToolBlock } from "./components/tool-block.js";
18
18
  import { argsFromResult } from "./tool-display.js";
19
19
 
20
20
  export function createCliOutputController({ state, terminalUi, transcript }) {
21
- const streamBuffer = createLegacyStreamBuffer();
22
21
  const legacyRenderer = new AssistantRenderer((text) => writeOutput(text));
23
22
  let assistantMessage = null;
24
23
  let blockCount = 0;
25
24
  const toolBlocks = new Map();
26
25
  const legacyBegunTools = new Set();
27
26
  let questionBlock = null;
28
-
29
- function suspendPrompt(action, options = {}) {
30
- return action();
31
- }
27
+ let turnContext = "";
32
28
 
33
29
  function redraw(force = false) {
34
30
  if (!terminalUi || state.runtime.status === "closing") {
@@ -39,19 +35,29 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
39
35
 
40
36
  function inputState() {
41
37
  const running = state.turn.active || state.display.activeCompact;
38
+ const inputSession = state.input.session;
42
39
  return {
43
40
  running,
44
- label: state.display.activeCompact
41
+ label: state.display.activityLabel || (state.display.activeCompact
45
42
  ? "Compacting"
46
- : state.display.goalChasing
47
- ? "Goal-Chasing"
48
- : "Working",
43
+ : "Working"),
49
44
  frame: state.display.activityFrame,
50
45
  elapsedMs: running ? Date.now() - state.display.activityStartedAt : 0,
51
46
  pendingInputs: state.input.pending,
47
+ inputMode: inputSession?.mode || "prompt",
48
+ menuOpen: Boolean(inputSession?.menuState?.matches?.()?.length),
52
49
  };
53
50
  }
54
51
 
52
+ function setActivityLabel(label = "") {
53
+ const next = String(label || "");
54
+ if (state.display.activityLabel === next) {
55
+ return;
56
+ }
57
+ state.display.activityLabel = next;
58
+ redraw();
59
+ }
60
+
55
61
  function mainPromptText(frameWidth) {
56
62
  return promptText(state.session.info, state.display.stats, inputState(), frameWidth);
57
63
  }
@@ -80,10 +86,9 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
80
86
  }
81
87
 
82
88
  function clearActivityTimer() {
83
- if (!state.display.activityTimer) {
84
- return;
89
+ if (state.display.activityTimer) {
90
+ clearInterval(state.display.activityTimer);
85
91
  }
86
- clearInterval(state.display.activityTimer);
87
92
  state.display.activityTimer = null;
88
93
  state.display.activityFrame = 0;
89
94
  state.display.activityStartedAt = 0;
@@ -114,7 +119,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
114
119
  function log(text) {
115
120
  const build = typeof text === "function" ? text : () => String(text ?? "");
116
121
  if (!terminalUi) {
117
- flushAssistantText(streamBuffer.flush());
118
122
  const value = String(build() ?? "");
119
123
  if (!value.trim()) {
120
124
  return;
@@ -129,7 +133,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
129
133
 
130
134
  function writeOutput(text) {
131
135
  if (!terminalUi) {
132
- flushAssistantText(streamBuffer.push(text));
136
+ flushAssistantText(text);
133
137
  }
134
138
  }
135
139
 
@@ -141,14 +145,13 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
141
145
  process.stdout.write(output);
142
146
  }
143
147
 
144
- function writeUserInput(text) {
148
+ function writeUserInput(text, source = "") {
145
149
  const value = String(text ?? "");
146
150
  if (!value.trim()) {
147
151
  return;
148
152
  }
149
153
  if (!terminalUi) {
150
- flushAssistantText(streamBuffer.flush());
151
- const line = userInputText(value);
154
+ const line = userInputText(value, undefined, source);
152
155
  if (!line) {
153
156
  return;
154
157
  }
@@ -158,7 +161,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
158
161
  }
159
162
  const leading = blockCount > 0;
160
163
  appendBlock(new DynamicBlock((width) => {
161
- const rendered = userInputText(value, width);
164
+ const rendered = userInputText(value, width, source);
162
165
  const lines = rendered ? rendered.split("\n") : [];
163
166
  if (leading && lines.length) {
164
167
  lines.unshift("");
@@ -179,8 +182,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
179
182
  appendBlock(new TextBlock(outputBlockText(value), { leading: blockCount > 0 }));
180
183
  }
181
184
 
182
- function closeOpenAssistantOutputLine() {}
183
-
184
185
  function ensureAssistantBlocks() {
185
186
  if (!assistantMessage) {
186
187
  const leading = blockCount > 0;
@@ -203,7 +204,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
203
204
  return;
204
205
  }
205
206
  legacyRenderer.finish();
206
- flushAssistantText(streamBuffer.flush());
207
207
  state.display.assistantHeaderShown = false;
208
208
  }
209
209
 
@@ -245,13 +245,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
245
245
  redraw();
246
246
  }
247
247
 
248
- function clearAssistantLineForInput() {
249
- if (!terminalUi && state.display.assistantOutputLineOpen) {
250
- process.stdout.write("\n");
251
- state.display.assistantOutputLineOpen = false;
252
- }
253
- }
254
-
255
248
  function showStartup(info) {
256
249
  if (!terminalUi) {
257
250
  return;
@@ -280,7 +273,8 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
280
273
  if (!callId) {
281
274
  return;
282
275
  }
283
- const existing = toolBlocks.get(callId);
276
+ const key = toolBlockKey(callId);
277
+ const existing = toolBlocks.get(key);
284
278
  if (existing) {
285
279
  existing.enrichArgs(event);
286
280
  return;
@@ -290,7 +284,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
290
284
  onRequestRender: () => redraw(),
291
285
  leading: blockCount > 0,
292
286
  });
293
- toolBlocks.set(callId, block);
287
+ toolBlocks.set(key, block);
294
288
  appendBlock(block);
295
289
  }
296
290
 
@@ -298,7 +292,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
298
292
  if (!terminalUi) {
299
293
  return;
300
294
  }
301
- toolBlocks.get(String(callId || ""))?.setProgress(message);
295
+ toolBlocks.get(toolBlockKey(callId))?.setProgress(message);
302
296
  }
303
297
 
304
298
  function finishTool(event, fileChange) {
@@ -307,7 +301,8 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
307
301
  return;
308
302
  }
309
303
  const callId = String(event?.tool_call_id || "");
310
- let block = toolBlocks.get(callId);
304
+ const key = toolBlockKey(callId);
305
+ let block = toolBlocks.get(key);
311
306
  if (!block) {
312
307
  if (!callId) {
313
308
  return;
@@ -317,7 +312,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
317
312
  onRequestRender: () => redraw(),
318
313
  leading: blockCount > 0,
319
314
  });
320
- toolBlocks.set(callId, block);
315
+ toolBlocks.set(key, block);
321
316
  appendBlock(block);
322
317
  }
323
318
  block.enrichArgs({ arguments: argsFromResult(event?.tool_name, event?.result) });
@@ -334,21 +329,30 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
334
329
  redraw();
335
330
  }
336
331
 
337
- function renderHistory(messages) {
338
- const pendingTools = new Map();
339
- const flushPendingTools = () => {
340
- for (const [toolCallId, tool] of pendingTools.entries()) {
341
- beginTool({
342
- tool_call_id: toolCallId,
343
- tool_name: tool.name,
344
- args_preview: tool.arguments,
345
- });
346
- finishTool({
347
- tool_call_id: toolCallId,
348
- tool_name: tool.name,
349
- status: "completed",
350
- result: "",
351
- });
332
+ function setTurnContext(turnId = "") {
333
+ turnContext = String(turnId || "");
334
+ }
335
+
336
+ function toolBlockKey(callId) {
337
+ const value = String(callId || "");
338
+ return value ? `${turnContext}:${value}` : "";
339
+ }
340
+
341
+ function renderHistory(messages) {
342
+ const pendingTools = new Map();
343
+ const flushPendingTools = () => {
344
+ for (const [toolCallId, tool] of pendingTools.entries()) {
345
+ beginTool({
346
+ tool_call_id: toolCallId,
347
+ tool_name: tool.name,
348
+ args_preview: tool.arguments,
349
+ });
350
+ finishTool({
351
+ tool_call_id: toolCallId,
352
+ tool_name: tool.name,
353
+ status: "completed",
354
+ result: "",
355
+ });
352
356
  }
353
357
  pendingTools.clear();
354
358
  };
@@ -360,39 +364,39 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
360
364
  writeUserInput(messageText(message?.content));
361
365
  continue;
362
366
  }
363
- if (role === "assistant") {
364
- flushPendingTools();
367
+ if (role === "assistant") {
368
+ flushPendingTools();
365
369
  const content = messageText(message?.content);
366
370
  if (content) {
367
371
  assistantAppend(content);
368
372
  closeAssistant();
369
373
  }
370
- for (const call of Array.isArray(message?.tool_calls) ? message.tool_calls : []) {
371
- const toolCallId = String(call?.id || "");
372
- const toolName = String(call?.function?.name || "tool");
373
- if (toolCallId) {
374
- pendingTools.set(toolCallId, {
375
- name: toolName,
376
- arguments: String(call?.function?.arguments || ""),
377
- });
378
- }
379
- }
380
- continue;
381
- }
382
- if (role === "tool") {
383
- const toolCallId = String(message?.tool_call_id || "");
384
- const tool = pendingTools.get(toolCallId) || { name: "tool", arguments: "" };
385
- pendingTools.delete(toolCallId);
386
- beginTool({
387
- tool_call_id: toolCallId,
388
- tool_name: tool.name,
389
- args_preview: tool.arguments,
390
- });
391
- finishTool({
392
- tool_call_id: toolCallId,
393
- tool_name: tool.name,
394
- status: "completed",
395
- result: messageText(message?.content),
374
+ for (const call of Array.isArray(message?.tool_calls) ? message.tool_calls : []) {
375
+ const toolCallId = String(call?.id || "");
376
+ const toolName = String(call?.function?.name || "tool");
377
+ if (toolCallId) {
378
+ pendingTools.set(toolCallId, {
379
+ name: toolName,
380
+ arguments: String(call?.function?.arguments || ""),
381
+ });
382
+ }
383
+ }
384
+ continue;
385
+ }
386
+ if (role === "tool") {
387
+ const toolCallId = String(message?.tool_call_id || "");
388
+ const tool = pendingTools.get(toolCallId) || { name: "tool", arguments: "" };
389
+ pendingTools.delete(toolCallId);
390
+ beginTool({
391
+ tool_call_id: toolCallId,
392
+ tool_name: tool.name,
393
+ args_preview: tool.arguments,
394
+ });
395
+ finishTool({
396
+ tool_call_id: toolCallId,
397
+ tool_name: tool.name,
398
+ status: "completed",
399
+ result: messageText(message?.content),
396
400
  });
397
401
  }
398
402
  }
@@ -403,16 +407,15 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
403
407
 
404
408
  return {
405
409
  terminalUi: Boolean(terminalUi),
406
- suspendPrompt,
407
410
  redraw,
408
411
  refreshInputState,
409
412
  clearActivityTimer,
413
+ setActivityLabel,
410
414
  mainPromptText,
411
415
  log,
412
416
  writeUserInput,
413
417
  writeError,
414
418
  closeAssistant,
415
- clearAssistantLineForInput,
416
419
  assistantAppend,
417
420
  beginTool,
418
421
  updateToolProgress,
@@ -420,28 +423,13 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
420
423
  beginQuestion,
421
424
  finishQuestion,
422
425
  setToolsExpanded,
426
+ setTurnContext,
423
427
  renderHistory,
424
428
  showStartup,
425
429
  replayAll: () => terminalUi?.replayAll?.(),
426
430
  };
427
431
  }
428
432
 
429
- function createLegacyStreamBuffer() {
430
- let pending = "";
431
- return {
432
- push(text) {
433
- pending += String(text || "");
434
- const flushed = pending;
435
- pending = "";
436
- return flushed;
437
- },
438
- flush() {
439
- const flushed = pending;
440
- pending = "";
441
- return flushed;
442
- },
443
- };
444
- }
445
433
 
446
434
  function messageText(content) {
447
435
  if (typeof content === "string") {
@@ -1,5 +1,5 @@
1
1
  import { REASONING_EFFORTS } from "./runtime-protocol.js";
2
- import { modelListErrorText, commandResultText, goalCommandText, sessionSwitchedText } from "./rendering.js";
2
+ import { commandResultText, contextBoardText, goalCommandText, modelListErrorText, sessionSwitchedText, usageBoardText } from "./rendering.js";
3
3
 
4
4
  export function createCliRuntimeController({
5
5
  client,
@@ -9,14 +9,16 @@ export function createCliRuntimeController({
9
9
  requireInitialization,
10
10
  state,
11
11
  getCommands,
12
- getTurnController,
13
12
  getTaskMonitor,
14
13
  getCompactContextState,
15
14
  askModelMenu,
16
15
  askEffortMenu = null,
17
16
  askSessionMenu,
17
+ askForkPointMenu,
18
+ askContextBoard = null,
18
19
  restoreLiveTurn,
19
20
  renderHistory = () => {},
21
+ onSessionRestored = () => {},
20
22
  clearPendingInputs,
21
23
  closeAssistant,
22
24
  refreshInputState,
@@ -74,7 +76,6 @@ export function createCliRuntimeController({
74
76
  }
75
77
 
76
78
  async function runGoalCommand(command) {
77
- const turnController = getTurnController();
78
79
  if (command.action === "set" && state.turn.active) {
79
80
  log(() => commandResultText("Goal not started", "pause or finish the active turn first"));
80
81
  return;
@@ -84,7 +85,6 @@ export function createCliRuntimeController({
84
85
  const result = await request(methods.goalSet, { objective: command.objective });
85
86
  updateGoalState(result?.goal);
86
87
  log(() => goalCommandText(result?.goal, "set"));
87
- turnController.submit(command.objective);
88
88
  return;
89
89
  }
90
90
  if (command.action === "clear") {
@@ -97,9 +97,6 @@ export function createCliRuntimeController({
97
97
  const result = await request(methods.goalStatus, { status: command.action === "resume" ? "active" : "paused" });
98
98
  updateGoalState(result?.goal);
99
99
  log(() => goalCommandText(result?.goal, command.action));
100
- if (command.action === "resume" && !state.turn.active) {
101
- turnController.submit("", { goal_continuation: true });
102
- }
103
100
  return;
104
101
  }
105
102
  const result = await request(methods.goalGet);
@@ -179,6 +176,7 @@ export function createCliRuntimeController({
179
176
  getCompactContextState().clear();
180
177
  refreshInputState();
181
178
  redraw();
179
+ await onSessionRestored();
182
180
  void getTaskMonitor()?.refresh().catch(() => {});
183
181
  return true;
184
182
  }
@@ -219,12 +217,137 @@ export function createCliRuntimeController({
219
217
  }
220
218
  }
221
219
 
220
+ async function runForkSelector() {
221
+ if (state.turn.active || state.display.activeCompact) {
222
+ log("Cannot fork while a turn is running. Wait for it to finish or stop it first.");
223
+ return;
224
+ }
225
+ if (String(state.session.info.session_type || "") === "delegated_task") {
226
+ log("Delegated task sessions cannot be forked.");
227
+ return;
228
+ }
229
+ let replay;
230
+ try {
231
+ replay = await request(methods.sessionReplay);
232
+ } catch (error) {
233
+ log(`Command failed: ${error instanceof Error ? error.message : String(error)}`);
234
+ return;
235
+ }
236
+ const messages = Array.isArray(replay?.messages) ? replay.messages : [];
237
+ const userMessages = messages.filter(isForkableUserMessage).slice(-FORK_MENU_MESSAGE_LIMIT);
238
+ if (!userMessages.length) {
239
+ log("Nothing to fork: this session has no messages yet.");
240
+ return;
241
+ }
242
+ const items = [
243
+ { id: "", label: FORK_END_LABEL },
244
+ ...userMessages.slice().reverse().map((message) => ({
245
+ id: String(message.id || ""),
246
+ label: forkPointLabel(message),
247
+ text: String(message.content || ""),
248
+ })),
249
+ ];
250
+ const selected = await askForkPointMenu(items);
251
+ if (!selected || state.runtime.status === "closing") {
252
+ return;
253
+ }
254
+ let fork;
255
+ try {
256
+ fork = await request(methods.sessionFork, selected.id ? { before_message_id: selected.id } : {});
257
+ } catch (error) {
258
+ log(`Fork failed: ${error instanceof Error ? error.message : String(error)}`);
259
+ return;
260
+ }
261
+ const newId = String(fork?.session_id || "");
262
+ if (!newId) {
263
+ log("Fork failed: the runtime returned no session id.");
264
+ return;
265
+ }
266
+ try {
267
+ const switched = await restoreSession(newId, {
268
+ switchSession: true,
269
+ announce: (info) => log(() => sessionSwitchedText(info)),
270
+ });
271
+ if (!switched) {
272
+ return;
273
+ }
274
+ } catch (error) {
275
+ log(`Forked to ${newId}, but switching failed: ${error instanceof Error ? error.message : String(error)}`);
276
+ return;
277
+ }
278
+ const kept = selected.id
279
+ ? Math.max(0, messages.findIndex((message) => String(message.id || "") === selected.id))
280
+ : messages.length;
281
+ log(`Forked ${newId} ← ${fork.forked_from} (${kept === messages.length ? `kept all ${kept}` : `kept the first ${kept} of ${messages.length}`} messages).`);
282
+ if (selected.text) {
283
+ state.input.prefill = selected.text;
284
+ }
285
+ }
286
+
287
+ async function runContextBoard() {
288
+ if (state.turn.active || state.display.activeCompact) {
289
+ log("Cannot open the context board while a turn is running. Wait for it to finish or stop it first.");
290
+ return;
291
+ }
292
+ let data;
293
+ try {
294
+ data = await fetchContextBoardData();
295
+ } catch (error) {
296
+ log(`Context board failed: ${error instanceof Error ? error.message : String(error)}`);
297
+ return;
298
+ }
299
+ if (!askContextBoard) {
300
+ printContextPages(data);
301
+ return;
302
+ }
303
+ await askContextBoard({
304
+ render: (pageIndex, width) => contextPageText(data, pageIndex, width),
305
+ });
306
+ }
307
+
308
+ async function printContextReport() {
309
+ let data;
310
+ try {
311
+ data = await fetchContextBoardData();
312
+ } catch (error) {
313
+ log(`Context report failed: ${error instanceof Error ? error.message : String(error)}`);
314
+ return;
315
+ }
316
+ printContextPages(data);
317
+ }
318
+
319
+ async function fetchContextBoardData() {
320
+ const breakdownResult = await request(methods.contextInspect);
321
+ const summaryResult = await request(methods.usageSummary, { days: CONTEXT_BOARD_DAYS });
322
+ return {
323
+ breakdown: breakdownResult?.breakdown && typeof breakdownResult.breakdown === "object" ? breakdownResult.breakdown : null,
324
+ latestUsage: breakdownResult?.latest_usage && typeof breakdownResult.latest_usage === "object" ? breakdownResult.latest_usage : null,
325
+ summary: summaryResult && typeof summaryResult === "object" ? summaryResult : null,
326
+ };
327
+ }
328
+
329
+ function contextPageText(data, pageIndex, width, { plain = false } = {}) {
330
+ return pageIndex === 0
331
+ ? contextBoardText({ breakdown: data.breakdown, latest_usage: data.latestUsage, index: 1, count: 2, plain }, width)
332
+ : usageBoardText({ summary: data.summary, index: 2, count: 2, plain }, width);
333
+ }
334
+
335
+ function printContextPages(data) {
336
+ for (const pageIndex of [0, 1]) {
337
+ const text = contextPageText(data, pageIndex, process.stdout.columns || 0, { plain: true });
338
+ if (text.trim()) {
339
+ log(text);
340
+ }
341
+ }
342
+ }
343
+
222
344
  function startCompactCommand() {
223
345
  if (state.display.activeCompact) {
224
346
  log("Compact is already running.");
225
347
  return;
226
348
  }
227
349
  state.display.activeCompact = true;
350
+ state.display.activityLabel = "Compacting";
228
351
  state.turn.interruptRequested = false;
229
352
  refreshInputState();
230
353
  void runCompactCommand().catch((error) => {
@@ -240,6 +363,7 @@ export function createCliRuntimeController({
240
363
  await getCommands().applyResult(result);
241
364
  } finally {
242
365
  state.display.activeCompact = false;
366
+ state.display.activityLabel = "";
243
367
  state.turn.interruptRequested = false;
244
368
  refreshInputState();
245
369
  }
@@ -309,12 +433,17 @@ export function createCliRuntimeController({
309
433
  runGoalCommand,
310
434
  refreshGoalState,
311
435
  runSessionsSelector,
436
+ runForkSelector,
437
+ runContextBoard,
438
+ printContextReport,
312
439
  startCompactCommand,
313
440
  runModelSelector,
314
441
  runEffortCommand,
315
442
  };
316
443
  }
317
444
 
445
+ const CONTEXT_BOARD_DAYS = 7;
446
+
318
447
  function mergeSlashCommands(...groups) {
319
448
  const byName = new Map();
320
449
  for (const group of groups) {
@@ -327,6 +456,24 @@ function mergeSlashCommands(...groups) {
327
456
  return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
328
457
  }
329
458
 
459
+ const FORK_MENU_MESSAGE_LIMIT = 50;
460
+ const FORK_END_LABEL = "Fork at current end (keep full history)";
461
+ const FORK_CONTEXT_KINDS = new Set(["skill_snapshot", "skill_catalog", "goal_checkpoint"]);
462
+
463
+ function isForkableUserMessage(message) {
464
+ if (message?.role !== "user" || !String(message?.content || "").trim()) {
465
+ return false;
466
+ }
467
+ const kind = message?.meta?.kind;
468
+ return !kind || !FORK_CONTEXT_KINDS.has(kind);
469
+ }
470
+
471
+ function forkPointLabel(message) {
472
+ const rawTime = String(message.ts || "").slice(11, 16);
473
+ const time = /^\d{2}:\d{2}$/.test(rawTime) ? rawTime : "";
474
+ return [time, singleLineText(message.content).slice(0, 60)].filter(Boolean).join(" · ");
475
+ }
476
+
330
477
  function sessionMenuOption(session) {
331
478
  const values = [singleLineText(session?.id), singleLineText(session?.title), singleLineText(session?.updated_at)];
332
479
  if (session?.current) values.push("current");
@@ -341,7 +488,7 @@ function modelSetResultText(result, model) {
341
488
  if (defaultModel) lines.push(`- default model: ${defaultModel} (unchanged)`);
342
489
  lines.push(result?.active_updated || result?.runtime || result?.session
343
490
  ? "- active session: updated"
344
- : "- active session: unchanged; start a new session to use this model");
491
+ : "- active turn: unchanged; the new model applies to the next turn");
345
492
  return commandResultText(lines[0], lines.slice(1).join(" · "));
346
493
  }
347
494
 
package/lib/cli-state.js CHANGED
@@ -25,13 +25,13 @@ export function createCliState() {
25
25
  },
26
26
  display: {
27
27
  activeCompact: false,
28
- goalChasing: false,
28
+ activityLabel: "",
29
29
  stats: {},
30
30
  lastEventSequence: 0,
31
+ lastTurnId: "",
31
32
  activityFrame: 0,
32
33
  activityTimer: null,
33
34
  activityStartedAt: 0,
34
- assistantOutputLineOpen: false,
35
35
  assistantHeaderShown: false,
36
36
  outputStarted: false,
37
37
  toolDetailsExpanded: false,
@@ -66,6 +66,28 @@ export function createCommandController({
66
66
  await input.runSessionsSelector?.();
67
67
  return;
68
68
  }
69
+ if (isBareForkCommand(text)) {
70
+ if (input.isTerminal && input.runForkSelector) {
71
+ await input.runForkSelector();
72
+ } else {
73
+ output.log?.("/fork requires an interactive terminal.");
74
+ }
75
+ return;
76
+ }
77
+ if (isContextCommand(text)) {
78
+ const argument = contextArgument(text);
79
+ if (argument) {
80
+ output.log?.("Custom ranges are not supported yet; showing the last 7 days.");
81
+ }
82
+ if (input.isTerminal && input.runContextBoard) {
83
+ await input.runContextBoard();
84
+ } else if (input.printContextReport) {
85
+ await input.printContextReport();
86
+ } else {
87
+ output.log?.("/context requires a connected runtime.");
88
+ }
89
+ return;
90
+ }
69
91
  const result = await request(runtimeMethods.commandExecute, { input: text });
70
92
  await applyResult(result);
71
93
  }
@@ -154,6 +176,18 @@ function isBareSessionsCommand(value) {
154
176
  return String(value || "").trim().toLowerCase() === "/sessions";
155
177
  }
156
178
 
179
+ function isBareForkCommand(value) {
180
+ return String(value || "").trim().toLowerCase() === "/fork";
181
+ }
182
+
183
+ function isContextCommand(value) {
184
+ return /^\/context\b/i.test(String(value || "").trim());
185
+ }
186
+
187
+ function contextArgument(value) {
188
+ return String(value || "").trim().replace(/^\/context\b/i, "").trim();
189
+ }
190
+
157
191
  function isCompactCommand(value) {
158
192
  return String(value || "").trim().toLowerCase() === "/compact";
159
193
  }