@goah/cli 0.11.5 → 0.13.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.
Files changed (160) hide show
  1. package/dist/cli.js +165 -66
  2. package/dist/cli.js.map +1 -1
  3. package/dist/console/assets/index-3sYrDMmt.css +1 -0
  4. package/dist/console/assets/index-Rtc_B7cs.js +52 -0
  5. package/dist/console/assets/index-Rtc_B7cs.js.map +1 -0
  6. package/dist/console/index.html +2 -2
  7. package/dist/control.d.ts +13 -13
  8. package/dist/control.d.ts.map +1 -1
  9. package/dist/control.js +77 -50
  10. package/dist/control.js.map +1 -1
  11. package/dist/index.d.ts +6 -23
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +156 -28
  14. package/dist/index.js.map +1 -1
  15. package/dist/live-config.js +2 -2
  16. package/dist/live-config.js.map +1 -1
  17. package/dist/prompt-queue.d.ts.map +1 -1
  18. package/dist/prompt-queue.js +5 -0
  19. package/dist/prompt-queue.js.map +1 -1
  20. package/dist/setup-wizard.d.ts +13 -2
  21. package/dist/setup-wizard.d.ts.map +1 -1
  22. package/dist/setup-wizard.js +87 -63
  23. package/dist/setup-wizard.js.map +1 -1
  24. package/dist/thread-inspect.d.ts +47 -0
  25. package/dist/thread-inspect.d.ts.map +1 -0
  26. package/dist/thread-inspect.js +78 -0
  27. package/dist/thread-inspect.js.map +1 -0
  28. package/dist/transcript.d.ts +2 -0
  29. package/dist/transcript.d.ts.map +1 -0
  30. package/dist/transcript.js +2 -0
  31. package/dist/transcript.js.map +1 -0
  32. package/dist/tui-theme.d.ts +1 -0
  33. package/dist/tui-theme.d.ts.map +1 -1
  34. package/dist/tui-theme.js +1 -0
  35. package/dist/tui-theme.js.map +1 -1
  36. package/dist/tui.d.ts +8 -2
  37. package/dist/tui.d.ts.map +1 -1
  38. package/dist/tui.js +291 -90
  39. package/dist/tui.js.map +1 -1
  40. package/dist/update.d.ts +1 -0
  41. package/dist/update.d.ts.map +1 -1
  42. package/dist/update.js +15 -7
  43. package/dist/update.js.map +1 -1
  44. package/dist/web-console.d.ts +1 -1
  45. package/dist/web-console.d.ts.map +1 -1
  46. package/dist/web-console.js +27 -37
  47. package/dist/web-console.js.map +1 -1
  48. package/dist/welcome.d.ts +2 -2
  49. package/dist/welcome.d.ts.map +1 -1
  50. package/dist/welcome.js +24 -36
  51. package/dist/welcome.js.map +1 -1
  52. package/node_modules/goah-ledger-contract/dist/execution.d.ts +272 -130
  53. package/node_modules/goah-ledger-contract/dist/execution.d.ts.map +1 -1
  54. package/node_modules/goah-ledger-contract/dist/execution.js +31 -19
  55. package/node_modules/goah-ledger-contract/dist/execution.js.map +1 -1
  56. package/node_modules/goah-ledger-contract/dist/index.d.ts +1 -2
  57. package/node_modules/goah-ledger-contract/dist/index.d.ts.map +1 -1
  58. package/node_modules/goah-ledger-contract/dist/index.js +1 -2
  59. package/node_modules/goah-ledger-contract/dist/index.js.map +1 -1
  60. package/node_modules/goah-ledger-contract/dist/kernel.d.ts +1 -1
  61. package/node_modules/goah-ledger-contract/dist/kernel.d.ts.map +1 -1
  62. package/node_modules/goah-ledger-contract/dist/kernel.js +1 -1
  63. package/node_modules/goah-ledger-contract/dist/kernel.js.map +1 -1
  64. package/node_modules/goah-ledger-contract/dist/transcript.d.ts +63 -0
  65. package/node_modules/goah-ledger-contract/dist/transcript.d.ts.map +1 -0
  66. package/node_modules/goah-ledger-contract/dist/transcript.js +115 -0
  67. package/node_modules/goah-ledger-contract/dist/transcript.js.map +1 -0
  68. package/node_modules/goah-ledger-contract/package.json +3 -4
  69. package/node_modules/goah-ledger-sqlite/dist/index.d.ts +32 -23
  70. package/node_modules/goah-ledger-sqlite/dist/index.d.ts.map +1 -1
  71. package/node_modules/goah-ledger-sqlite/dist/index.js +764 -394
  72. package/node_modules/goah-ledger-sqlite/dist/index.js.map +1 -1
  73. package/node_modules/goah-ledger-sqlite/package.json +2 -2
  74. package/node_modules/goah-runner-pi/dist/configurator.d.ts.map +1 -1
  75. package/node_modules/goah-runner-pi/dist/configurator.js +186 -30
  76. package/node_modules/goah-runner-pi/dist/configurator.js.map +1 -1
  77. package/node_modules/goah-runner-pi/dist/credential-store.d.ts +2 -0
  78. package/node_modules/goah-runner-pi/dist/credential-store.d.ts.map +1 -1
  79. package/node_modules/goah-runner-pi/dist/credential-store.js +117 -23
  80. package/node_modules/goah-runner-pi/dist/credential-store.js.map +1 -1
  81. package/node_modules/goah-runner-pi/dist/index.d.ts +18 -6
  82. package/node_modules/goah-runner-pi/dist/index.d.ts.map +1 -1
  83. package/node_modules/goah-runner-pi/dist/index.js +97 -12
  84. package/node_modules/goah-runner-pi/dist/index.js.map +1 -1
  85. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts +4 -2
  86. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts.map +1 -1
  87. package/node_modules/goah-runner-pi/dist/pi-worker.js +220 -69
  88. package/node_modules/goah-runner-pi/dist/pi-worker.js.map +1 -1
  89. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts +2 -0
  90. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts.map +1 -0
  91. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js +3 -0
  92. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js.map +1 -0
  93. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts +2 -0
  94. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts.map +1 -0
  95. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js +5 -0
  96. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js.map +1 -0
  97. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts +2 -0
  98. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts.map +1 -0
  99. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js +5 -0
  100. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js.map +1 -0
  101. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts +1 -1
  102. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts.map +1 -1
  103. package/node_modules/goah-runner-pi/dist/test-helpers.js +3 -1
  104. package/node_modules/goah-runner-pi/dist/test-helpers.js.map +1 -1
  105. package/node_modules/goah-runner-pi/dist/verification-worker.d.ts.map +1 -1
  106. package/node_modules/goah-runner-pi/dist/verification-worker.js +3 -5
  107. package/node_modules/goah-runner-pi/dist/verification-worker.js.map +1 -1
  108. package/node_modules/goah-runner-pi/package.json +2 -2
  109. package/node_modules/goah-supervisor/dist/context-view.d.ts +4 -5
  110. package/node_modules/goah-supervisor/dist/context-view.d.ts.map +1 -1
  111. package/node_modules/goah-supervisor/dist/context-view.js +7 -20
  112. package/node_modules/goah-supervisor/dist/context-view.js.map +1 -1
  113. package/node_modules/goah-supervisor/dist/index.d.ts +38 -36
  114. package/node_modules/goah-supervisor/dist/index.d.ts.map +1 -1
  115. package/node_modules/goah-supervisor/dist/index.js +762 -575
  116. package/node_modules/goah-supervisor/dist/index.js.map +1 -1
  117. package/node_modules/goah-supervisor/dist/roles.d.ts +2 -1
  118. package/node_modules/goah-supervisor/dist/roles.d.ts.map +1 -1
  119. package/node_modules/goah-supervisor/dist/roles.js +20 -5
  120. package/node_modules/goah-supervisor/dist/roles.js.map +1 -1
  121. package/node_modules/goah-supervisor/dist/verification.d.ts +8 -29
  122. package/node_modules/goah-supervisor/dist/verification.d.ts.map +1 -1
  123. package/node_modules/goah-supervisor/dist/verification.js +71 -46
  124. package/node_modules/goah-supervisor/dist/verification.js.map +1 -1
  125. package/node_modules/goah-supervisor/package.json +2 -2
  126. package/node_modules/goah-testkit/dist/faux-runner-worker.js +16 -9
  127. package/node_modules/goah-testkit/dist/faux-runner-worker.js.map +1 -1
  128. package/node_modules/goah-testkit/dist/index.d.ts +13 -16
  129. package/node_modules/goah-testkit/dist/index.d.ts.map +1 -1
  130. package/node_modules/goah-testkit/dist/index.js +27 -44
  131. package/node_modules/goah-testkit/dist/index.js.map +1 -1
  132. package/node_modules/goah-testkit/package.json +5 -5
  133. package/package.json +7 -8
  134. package/dist/console/assets/index-BJTGvlCH.js +0 -52
  135. package/dist/console/assets/index-BJTGvlCH.js.map +0 -1
  136. package/dist/console/assets/index-msBn41Cl.css +0 -1
  137. package/dist/inspect.d.ts +0 -61
  138. package/dist/inspect.d.ts.map +0 -1
  139. package/dist/inspect.js +0 -110
  140. package/dist/inspect.js.map +0 -1
  141. package/dist/metrics.d.ts +0 -2
  142. package/dist/metrics.d.ts.map +0 -1
  143. package/dist/metrics.js +0 -2
  144. package/dist/metrics.js.map +0 -1
  145. package/dist/session.d.ts +0 -2
  146. package/dist/session.d.ts.map +0 -1
  147. package/dist/session.js +0 -2
  148. package/dist/session.js.map +0 -1
  149. package/node_modules/goah-ledger-contract/dist/metrics.d.ts +0 -40
  150. package/node_modules/goah-ledger-contract/dist/metrics.d.ts.map +0 -1
  151. package/node_modules/goah-ledger-contract/dist/metrics.js +0 -25
  152. package/node_modules/goah-ledger-contract/dist/metrics.js.map +0 -1
  153. package/node_modules/goah-ledger-contract/dist/session.d.ts +0 -63
  154. package/node_modules/goah-ledger-contract/dist/session.d.ts.map +0 -1
  155. package/node_modules/goah-ledger-contract/dist/session.js +0 -115
  156. package/node_modules/goah-ledger-contract/dist/session.js.map +0 -1
  157. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts +0 -2
  158. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts.map +0 -1
  159. package/node_modules/goah-testkit/dist/mock-connector-worker.js +0 -29
  160. package/node_modules/goah-testkit/dist/mock-connector-worker.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /** Full-screen goah TUI: streaming CEO transcript over the resident Supervisor control socket. */
2
2
  import { TuiAltScreen, Text, Markdown, Editor, ProcessTerminal, ScrollView, VStack } from "@earendil-works/pi-tui";
3
3
  import { controlAvailable, requestControl, streamControl } from "./control.js";
4
- import { readConsoleMetadata } from "./index.js";
4
+ import { loadConfig, readConsoleMetadata, readDefaultRunnerProfile } from "./index.js";
5
5
  import { switchModel, reloadDaemon, readRunnerDisplay } from "./live-config.js";
6
6
  import { welcomeSnapshot, renderWelcome } from "./welcome.js";
7
- import { runSetupWizard, applyWizardResult } from "./setup-wizard.js";
7
+ import { chooseSetupSection, runRunnerCommandWizard, runSetupWizard, applyWizardResult } from "./setup-wizard.js";
8
8
  import { installedVersion } from "./update.js";
9
9
  import { tuiTheme } from "./tui-theme.js";
10
10
  import { spawn } from "node:child_process";
11
- import { closeSync, mkdirSync, openSync } from "node:fs";
11
+ import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
12
12
  import { join, resolve } from "node:path";
13
13
  class HeaderBar {
14
14
  runner;
@@ -43,11 +43,16 @@ const markdownTheme = {
43
43
  class ConversationView {
44
44
  entries;
45
45
  liveMarkdown = "";
46
- thinking = false;
46
+ liveThinking = "";
47
+ thinkingActive = false;
47
48
  constructor(initial) { this.entries = [{ kind: "text", content: initial.join("\n") }]; }
48
49
  addText(content) { this.entries.push({ kind: "text", content }); this.trim(); }
50
+ addUser(content) { this.entries.push({ kind: "user", content }); this.trim(); }
49
51
  addMarkdown(content) {
50
52
  this.liveMarkdown = "";
53
+ content = content.trim();
54
+ if (!content)
55
+ return;
51
56
  const previous = this.entries.at(-1);
52
57
  if (previous?.kind === "markdown" && previous.content === content)
53
58
  return;
@@ -56,7 +61,30 @@ class ConversationView {
56
61
  }
57
62
  setLiveMarkdown(content) { this.liveMarkdown = content; }
58
63
  clearLiveMarkdown() { this.liveMarkdown = ""; }
59
- setThinking(active) { this.thinking = active; }
64
+ updateThinking(activity) {
65
+ if (activity.phase === "start") {
66
+ this.liveThinking = "";
67
+ this.thinkingActive = true;
68
+ return;
69
+ }
70
+ if (activity.phase === "delta") {
71
+ this.liveThinking += activity.text;
72
+ this.thinkingActive = true;
73
+ return;
74
+ }
75
+ if (activity.phase === "clear") {
76
+ this.liveThinking = "";
77
+ this.thinkingActive = false;
78
+ return;
79
+ }
80
+ const content = (activity.text || this.liveThinking).trim();
81
+ this.liveThinking = "";
82
+ this.thinkingActive = false;
83
+ const previous = this.entries.at(-1);
84
+ if (content && !(previous?.kind === "thinking" && previous.content === content))
85
+ this.entries.push({ kind: "thinking", content });
86
+ this.trim();
87
+ }
60
88
  updateTool(activity) {
61
89
  const previous = this.entries.findLast((entry) => entry.kind === "tool" && entry.callId === activity.callId);
62
90
  if (previous?.kind === "tool")
@@ -69,18 +97,25 @@ class ConversationView {
69
97
  this.entries.splice(1, this.entries.length - 240); }
70
98
  render(width) {
71
99
  const rendered = this.entries.flatMap((entry) => entry.kind === "markdown"
72
- ? [tuiTheme.accent(" goah"), ...new Markdown(entry.content, 2, 0, markdownTheme).render(width), ""]
73
- : entry.kind === "tool"
74
- ? new Text(toolActivityLine(entry), 2, 0).render(width)
75
- : new Text(entry.content, 2, 0).render(width));
76
- if (this.thinking)
100
+ ? [...new Markdown(entry.content, 2, 0, markdownTheme).render(width), ""]
101
+ : entry.kind === "user"
102
+ ? renderUserMessage(entry.content, width)
103
+ : entry.kind === "thinking"
104
+ ? [tuiTheme.muted(" thinking"), ...new Markdown(entry.content, 2, 0, markdownTheme, { color: tuiTheme.muted, italic: true }).render(width), ""]
105
+ : entry.kind === "tool"
106
+ ? new Text(toolActivityLine(entry), 2, 0).render(width)
107
+ : new Text(entry.content, 2, 0).render(width));
108
+ if (this.liveThinking)
109
+ rendered.push(tuiTheme.muted(" thinking"), ...new Markdown(this.liveThinking, 2, 0, markdownTheme, { color: tuiTheme.muted, italic: true }).render(width));
110
+ else if (this.thinkingActive)
77
111
  rendered.push(tuiTheme.muted(" thinking…"));
78
112
  if (this.liveMarkdown)
79
- rendered.push(tuiTheme.accent(" goah"), ...new Markdown(this.liveMarkdown, 2, 0, markdownTheme).render(width));
113
+ rendered.push(...new Markdown(this.liveMarkdown, 2, 0, markdownTheme).render(width));
80
114
  return rendered;
81
115
  }
82
116
  invalidate() { }
83
117
  }
118
+ export function renderUserMessage(content, width) { return new Text(content, 2, 1, tuiTheme.userMessage).render(width); }
84
119
  export function renderTuiHeader(width, runner, target, version) {
85
120
  const brand = " GOAH ";
86
121
  const release = width >= 34 ? ` v${version} ` : "";
@@ -115,13 +150,17 @@ export function classifyTuiInput(value, busy) {
115
150
  return { action: "stop", text };
116
151
  if (text === "/model" || text.startsWith("/model "))
117
152
  return { action: "model", text };
118
- if (text === "/setup")
153
+ if (text === "/login" || text.startsWith("/login "))
154
+ return { action: "login", text };
155
+ if (text === "/logout" || text.startsWith("/logout "))
156
+ return { action: "logout", text };
157
+ if (text === "/setup" || text.startsWith("/setup "))
119
158
  return { action: "setup", text };
120
159
  if (text.startsWith("/goal ") || text.startsWith("/observe "))
121
160
  return { action: "goal", text };
122
- if (text.startsWith("/approve ") || text.startsWith("/reject "))
123
- return { action: "approval", text };
124
- return { action: busy ? "queue" : "send", text };
161
+ if (text.startsWith("/"))
162
+ return { action: "unknown", text };
163
+ return { action: busy ? "steer" : "send", text };
125
164
  }
126
165
  export async function runGoahTui(configPath, stateDir, initialMessage) {
127
166
  if (!process.stdout.isTTY || !process.stdin.isTTY)
@@ -130,11 +169,19 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
130
169
  const snapshot = welcomeSnapshot(stateDir, runner);
131
170
  const welcome = renderWelcome(snapshot, Boolean(snapshot.root || snapshot.handoffs.length || snapshot.conversation.length));
132
171
  await ensureDaemon(configPath, stateDir);
172
+ const liveSnapshot = await requestControl(stateDir, { op: "status" }).catch(() => null);
173
+ const liveTurns = liveSnapshot && typeof liveSnapshot === "object" && !Array.isArray(liveSnapshot) && Array.isArray(liveSnapshot.turns) ? liveSnapshot.turns : [];
174
+ const liveInteractionTurnId = findLiveTurnId(liveTurns);
133
175
  const terminal = new ProcessTerminal();
134
176
  const tui = new TuiAltScreen(terminal, true, undefined, { mouse: true });
135
177
  terminal.setTitle(`Goah · ${runner.target}`);
136
178
  const headerView = new HeaderBar(runner.runner, runner.target, installedVersion());
137
179
  const transcriptView = new ConversationView(welcome);
180
+ for (const row of snapshot.conversation)
181
+ if (row.speaker === "You")
182
+ transcriptView.addUser(row.text);
183
+ else
184
+ transcriptView.addMarkdown(row.text);
138
185
  const conversationScroll = new ScrollView(transcriptView, { follow: "end", primary: true, scrollbar: "auto", scrollbarStyle: tuiTheme.muted });
139
186
  const goalView = new GoalBar(snapshot.root);
140
187
  const input = new Editor(tui, {
@@ -151,7 +198,11 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
151
198
  ]);
152
199
  const busy = { active: false };
153
200
  const queued = [];
201
+ const queuedTurnIds = [];
154
202
  let activeStream = null;
203
+ let activeInteractionTurnId = null;
204
+ let steeringTail = Promise.resolve();
205
+ let configuring = false;
155
206
  let exiting = false;
156
207
  const push = (line) => {
157
208
  transcriptView.addText(line);
@@ -159,23 +210,29 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
159
210
  };
160
211
  let liveText = "";
161
212
  const appendLive = (text) => { liveText += text; transcriptView.setLiveMarkdown(liveText); tui.requestRender(); };
162
- const commitLive = (text) => { liveText = ""; if (text)
213
+ const commitLive = (text) => { liveText = ""; transcriptView.clearLiveMarkdown(); if (text)
163
214
  transcriptView.addMarkdown(text); tui.requestRender(); };
164
215
  const pushResponse = (text) => { transcriptView.addMarkdown(text); tui.requestRender(); };
165
216
  const updateTool = (activity) => { transcriptView.updateTool(activity); tui.requestRender(); };
166
- const setThinking = (active) => { transcriptView.setThinking(active); tui.requestRender(); };
167
- const send = async (message) => {
217
+ const updateThinking = (activity) => { transcriptView.updateThinking(activity); tui.requestRender(); };
218
+ const setWakeState = (mode) => { statusView.setText(statusText(mode, mode === "queued" ? 1 : queued.length)); tui.requestRender(); };
219
+ const send = async (message, showUser = true) => {
168
220
  busy.active = true;
169
221
  const controller = new AbortController();
170
222
  activeStream = controller;
171
223
  statusView.setText(statusText("working", queued.length));
172
- push(`${tuiTheme.human("you")} ${message}`);
224
+ if (showUser) {
225
+ transcriptView.addUser(message);
226
+ tui.requestRender();
227
+ }
173
228
  try {
174
- await streamControl(stateDir, { op: "interact", message }, (frame) => renderFrame(frame, push, appendLive, commitLive, pushResponse, updateTool, setThinking), controller.signal);
229
+ await streamControl(stateDir, { op: "interact", message }, (frame) => { if (frame.type === "accepted")
230
+ activeInteractionTurnId = frame.turnId; if (frame.type === "result" || frame.type === "error")
231
+ activeInteractionTurnId = null; renderFrame(frame, push, appendLive, commitLive, pushResponse, updateTool, updateThinking, setWakeState, () => commitLive("")); }, controller.signal);
175
232
  }
176
233
  catch (error) {
177
234
  transcriptView.clearLiveMarkdown();
178
- transcriptView.setThinking(false);
235
+ transcriptView.updateThinking({ phase: "clear", text: "" });
179
236
  if (!controller.signal.aborted)
180
237
  push(errorLine(error));
181
238
  }
@@ -184,30 +241,153 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
184
241
  activeStream = null;
185
242
  busy.active = false;
186
243
  await refreshGoalBar(stateDir, goalView, tui);
187
- statusView.setText(statusText(queued.length ? "queued" : "ready", queued.length));
188
- const next = exiting ? undefined : queued.shift();
189
- if (next)
190
- void send(next);
244
+ statusView.setText(statusText(queuedTurnIds.length || queued.length ? "queued" : "ready", queuedTurnIds.length + queued.length));
245
+ continuePending();
191
246
  }
192
247
  };
193
- const launchSetup = async () => {
194
- if (busy.active) {
195
- push("Wait for the current wake or use /stop before changing Runner configuration.");
248
+ const attachTurn = async (turnId) => {
249
+ busy.active = true;
250
+ activeInteractionTurnId = turnId;
251
+ const controller = new AbortController();
252
+ activeStream = controller;
253
+ statusView.setText(statusText("queued", 1));
254
+ try {
255
+ await streamControl(stateDir, { op: "turn.attach", turnId }, (frame) => { if (frame.type === "result" || frame.type === "error")
256
+ activeInteractionTurnId = null; renderFrame(frame, push, appendLive, commitLive, pushResponse, updateTool, updateThinking, setWakeState, () => commitLive("")); }, controller.signal);
257
+ }
258
+ catch (error) {
259
+ if (!controller.signal.aborted)
260
+ push(errorLine(error));
261
+ }
262
+ finally {
263
+ if (activeStream === controller)
264
+ activeStream = null;
265
+ busy.active = false;
266
+ await refreshGoalBar(stateDir, goalView, tui);
267
+ statusView.setText(statusText("ready"));
268
+ continuePending();
269
+ }
270
+ };
271
+ const continuePending = () => {
272
+ if (exiting || configuring || busy.active)
196
273
  return;
274
+ const turnId = queuedTurnIds.shift();
275
+ if (turnId) {
276
+ void attachTurn(turnId);
277
+ return;
278
+ }
279
+ const next = queued.shift();
280
+ if (next)
281
+ void send(next, false);
282
+ };
283
+ const steer = async (message) => {
284
+ try {
285
+ const outcome = await requestControl(stateDir, { op: "turn.steer", message });
286
+ const value = outcome && typeof outcome === "object" && !Array.isArray(outcome) ? outcome : {};
287
+ const queuedBySupervisor = value.steered === false && typeof value.turnId === "string";
288
+ if (queuedBySupervisor) {
289
+ queuedTurnIds.push(String(value.turnId));
290
+ activeStream?.abort();
291
+ }
292
+ statusView.setText(queuedBySupervisor ? `${tuiTheme.warning("new Turn")} ${tuiTheme.muted("Runner stopped accepting steering; continuing in a fresh Turn")}` : `${tuiTheme.accent("working")} ${tuiTheme.muted("your update is steering this turn · /stop cancels")}`);
293
+ tui.requestRender();
294
+ if (queuedBySupervisor && !busy.active)
295
+ continuePending();
296
+ }
297
+ catch (error) {
298
+ const reason = error instanceof Error ? error.message : String(error);
299
+ if (reason.includes("no longer accepting steering messages")) {
300
+ queued.push(message);
301
+ statusView.setText(statusText("working", queued.length));
302
+ if (!busy.active)
303
+ continuePending();
304
+ }
305
+ else
306
+ push(errorLine(error));
307
+ }
308
+ };
309
+ const submitSteer = (message) => {
310
+ transcriptView.addUser(message);
311
+ tui.requestRender();
312
+ steeringTail = steeringTail.then(() => exiting ? undefined : steer(message));
313
+ };
314
+ const withConfigurationScreen = async (work) => {
315
+ if (busy.active) {
316
+ const snapshot = await requestControl(stateDir, { op: "status" }).catch(() => null);
317
+ if (!snapshot) {
318
+ push("Cannot inspect the current turn; use /stop before changing Runner configuration.");
319
+ return;
320
+ }
321
+ const turns = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && Array.isArray(snapshot.turns) ? snapshot.turns : [];
322
+ if (turns.some((turn) => turn.status === "in_progress")) {
323
+ push("Wait for the current Turn or use /stop before changing Runner configuration.");
324
+ return;
325
+ }
197
326
  }
327
+ else
328
+ configuring = true;
198
329
  statusView.setText(statusText("setup"));
199
330
  tui.stop();
200
331
  try {
201
- await runSetupReload(configPath, stateDir, push);
332
+ await work();
333
+ }
334
+ catch (error) {
335
+ push(errorLine(error));
202
336
  }
203
337
  finally {
338
+ configuring = false;
204
339
  if (!exiting) {
205
340
  tui.start();
206
341
  tui.requestRender(true);
207
342
  statusView.setText(statusText("ready"));
343
+ continuePending();
208
344
  }
209
345
  }
210
346
  };
347
+ const launchRunnerCommand = async (command, commandArgs = []) => withConfigurationScreen(async () => {
348
+ const current = configuredRunnerProfile(configPath);
349
+ if (!current)
350
+ throw new Error("No Runner Profile is configured; use /setup first.");
351
+ const result = await runRunnerCommandWizard(current, command, commandArgs);
352
+ try {
353
+ await applyWizardResult({ profile: result.profile }, existsSync(configPath) ? configPath : null);
354
+ }
355
+ catch (error) {
356
+ await result.rollback?.();
357
+ throw error;
358
+ }
359
+ for (const line of result.output)
360
+ push(line);
361
+ push(await reloadDaemon(stateDir, configPath) ? "Configuration updated — applies to the next Turn." : "Configuration saved — restart Goah to apply it.");
362
+ });
363
+ const launchSetup = async (text) => withConfigurationScreen(async () => {
364
+ const current = configuredRunnerProfile(configPath);
365
+ const requested = text.slice("/setup".length).trim();
366
+ if (requested && !["runner", "model", "auth"].includes(requested))
367
+ throw new Error("usage: /setup [runner|model|auth]");
368
+ const section = requested || (current ? await chooseSetupSection(current) : "runner");
369
+ if (!section)
370
+ return;
371
+ if (section === "runner" || !current) {
372
+ const result = await runSetupWizard(current);
373
+ if (!result.profile)
374
+ return;
375
+ await applyWizardResult(result, existsSync(configPath) ? configPath : null);
376
+ push(await reloadDaemon(stateDir, configPath) ? "Runner profile updated — applies to the next Turn." : "Runner profile saved — restart Goah to apply it.");
377
+ return;
378
+ }
379
+ const result = await runRunnerCommandWizard(current, section === "model" ? "model" : "auth");
380
+ try {
381
+ await applyWizardResult({ profile: result.profile }, existsSync(configPath) ? configPath : null);
382
+ }
383
+ catch (error) {
384
+ await result.rollback?.();
385
+ throw error;
386
+ }
387
+ for (const line of result.output)
388
+ push(line);
389
+ push(await reloadDaemon(stateDir, configPath) ? "Configuration updated — applies to the next Turn." : "Configuration saved — restart Goah to apply it.");
390
+ });
211
391
  input.onSubmit = (line) => {
212
392
  const { action, text } = classifyTuiInput(line, busy.active);
213
393
  input.setText("");
@@ -219,7 +399,7 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
219
399
  return;
220
400
  }
221
401
  if (action === "help") {
222
- push(`${tuiTheme.strong("Commands")}\n /model /setup /status /records /history\n /goal /observe /approve /reject /stop /quit\n`);
402
+ push(`${tuiTheme.strong("Commands")}\n /model /login /logout /setup /status\n /records /history /goal /observe /stop /quit\n`);
223
403
  return;
224
404
  }
225
405
  if (action === "status") {
@@ -236,27 +416,33 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
236
416
  }
237
417
  if (action === "model") {
238
418
  if (text === "/model")
239
- void launchSetup();
419
+ void launchRunnerCommand("model");
240
420
  else
241
421
  void switchModelCommand(text, configPath, stateDir, push);
242
422
  return;
243
423
  }
424
+ if (action === "login") {
425
+ void launchRunnerCommand("auth", ["login", text.slice("/login".length).trim()].filter(Boolean));
426
+ return;
427
+ }
428
+ if (action === "logout") {
429
+ void launchRunnerCommand("auth", ["logout", text.slice("/logout".length).trim()].filter(Boolean));
430
+ return;
431
+ }
244
432
  if (action === "setup") {
245
- void launchSetup();
433
+ void launchSetup(text);
246
434
  return;
247
435
  }
248
436
  if (action === "goal") {
249
437
  void slashGoal(text, stateDir, push).finally(() => refreshGoalBar(stateDir, goalView, tui));
250
438
  return;
251
439
  }
252
- if (action === "approval") {
253
- void slashApprove(text, stateDir, push);
440
+ if (action === "unknown") {
441
+ push(errorLine(`Unknown command: ${text.split(/\s+/, 1)[0]}. Use /help to list commands.`));
254
442
  return;
255
443
  }
256
- if (action === "queue") {
257
- queued.push(text);
258
- push(tuiTheme.muted(`queued ${text}`));
259
- statusView.setText(statusText("working", queued.length));
444
+ if (action === "steer") {
445
+ submitSteer(text);
260
446
  return;
261
447
  }
262
448
  if (action === "send")
@@ -269,7 +455,7 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
269
455
  if (data !== "\x03")
270
456
  return undefined;
271
457
  exiting = true;
272
- push(busy.active ? "Detached — the current wake continues in the daemon. Use /stop before detaching when you intend to cancel it." : "Detached.");
458
+ push(busy.active ? "Detached — the current Turn continues in the daemon. Use /stop before detaching when you intend to cancel it." : "Detached.");
273
459
  activeStream?.abort();
274
460
  tui.stop();
275
461
  resolveExit();
@@ -278,8 +464,14 @@ export async function runGoahTui(configPath, stateDir, initialMessage) {
278
464
  tui.start();
279
465
  if (initialMessage)
280
466
  void send(initialMessage);
467
+ else if (typeof liveInteractionTurnId === "string")
468
+ void attachTurn(liveInteractionTurnId);
281
469
  await exited;
282
470
  }
471
+ export function findLiveTurnId(turns) {
472
+ const id = [...turns].reverse().find((turn) => turn.status === "in_progress" && turn.source === "human")?.id;
473
+ return typeof id === "string" ? id : null;
474
+ }
283
475
  async function runNonInteractive(configPath, stateDir, initialMessage) {
284
476
  await ensureDaemon(configPath, stateDir);
285
477
  console.log(`Console: ${(await waitForConsoleMetadata(stateDir)).url}`);
@@ -318,20 +510,28 @@ function frameToLine(frame) {
318
510
  return lines.length ? lines.join("\n") : null;
319
511
  }
320
512
  /** Render one structured control-protocol frame into its own transcript channel. */
321
- export function renderFrame(frame, push, appendLive = push, commitLive = push, pushResponse = push, updateTool = (activity) => push(toolActivityLine(activity)), setThinking = () => { }) {
513
+ export function renderFrame(frame, push, appendLive = push, commitLive = push, pushResponse = push, updateTool = (activity) => push(toolActivityLine(activity)), updateThinking = () => { }, setWakeState = () => { }, clearLive = () => { }) {
322
514
  if (frame.type === "error") {
323
- setThinking(false);
515
+ updateThinking({ phase: "clear", text: "" });
516
+ clearLive();
324
517
  push(`${tuiTheme.error("error")} ${safeError(frame.error)}\n`);
325
518
  return;
326
519
  }
327
- if (frame.type === "accepted")
520
+ if (frame.type === "accepted") {
521
+ const value = frame.value && typeof frame.value === "object" && !Array.isArray(frame.value) ? frame.value : {};
522
+ setWakeState(value.steered === true ? "working" : "queued");
328
523
  return;
524
+ }
329
525
  if (frame.type === "result") {
330
526
  const value = frame.value && typeof frame.value === "object" && !Array.isArray(frame.value) ? frame.value : {};
331
527
  const response = value.response && typeof value.response === "object" && !Array.isArray(value.response) ? value.response : {};
332
- setThinking(false);
528
+ const turn = value.turn && typeof value.turn === "object" && !Array.isArray(value.turn) ? value.turn : {};
529
+ const error = turn.error && typeof turn.error === "object" && !Array.isArray(turn.error) ? turn.error : {};
530
+ updateThinking({ phase: "done", text: "" });
333
531
  if (typeof response.content === "string" && response.content.trim())
334
532
  pushResponse(response.content.trim());
533
+ else if (turn.status === "failed" && typeof error.message === "string")
534
+ push(`${tuiTheme.error("error")} ${safeError(error.message)}\n`);
335
535
  return;
336
536
  }
337
537
  if (frame.type !== "event")
@@ -341,8 +541,16 @@ export function renderFrame(frame, push, appendLive = push, commitLive = push, p
341
541
  return;
342
542
  const record = event;
343
543
  const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : {};
544
+ if (record.type === "wake.enqueued") {
545
+ setWakeState("queued");
546
+ return;
547
+ }
548
+ if (record.type === "turn.started" || record.type === "transcript.started") {
549
+ setWakeState("working");
550
+ return;
551
+ }
344
552
  if (record.type === "tool.called") {
345
- setThinking(false);
553
+ updateThinking({ phase: "done", text: "" });
346
554
  updateTool({ kind: "tool", callId: String(data.callId ?? "tool"), name: String(data.name ?? "tool"), detail: toolDetail(data.arguments), status: "running" });
347
555
  }
348
556
  else if (record.type === "tool.completed") {
@@ -350,39 +558,50 @@ export function renderFrame(frame, push, appendLive = push, commitLive = push, p
350
558
  }
351
559
  else if (record.type === "message.assistant.delta") {
352
560
  const delta = data.delta && typeof data.delta === "object" && !Array.isArray(data.delta) ? data.delta : {};
353
- if (delta.type === "thinking_start" || delta.type === "thinking_delta")
354
- setThinking(true);
561
+ if (delta.type === "thinking_start")
562
+ updateThinking({ phase: "start", text: "" });
563
+ else if (delta.type === "thinking_delta" && typeof delta.delta === "string")
564
+ updateThinking({ phase: "delta", text: delta.delta });
565
+ else if (delta.type === "thinking_end")
566
+ updateThinking({ phase: "done", text: typeof delta.content === "string" ? delta.content : "" });
355
567
  else if (delta.type === "text_start" || delta.type === "text_delta") {
356
- setThinking(false);
568
+ updateThinking({ phase: "done", text: "" });
357
569
  if (delta.type === "text_delta" && typeof delta.delta === "string")
358
570
  appendLive(delta.delta);
359
571
  }
360
572
  else if (delta.type === "toolcall_start" || delta.type === "toolcall_delta")
361
- setThinking(false);
573
+ updateThinking({ phase: "done", text: "" });
362
574
  }
363
575
  else if (record.type === "message.assistant.completed") {
364
- setThinking(false);
576
+ updateThinking({ phase: "done", text: "" });
365
577
  const message = data.message && typeof data.message === "object" ? data.message : {};
578
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
579
+ clearLive();
580
+ return;
581
+ }
366
582
  const text = messageText(message.content);
583
+ if (data.completionIntent === "handoff") {
584
+ clearLive();
585
+ return;
586
+ }
367
587
  if (text)
368
588
  commitLive(text);
369
589
  }
590
+ else if (record.type === "response.committed") {
591
+ if (typeof data.text === "string" && data.text.trim())
592
+ commitLive(data.text.trim());
593
+ }
370
594
  else if (record.type === "handoff.recorded") {
371
595
  if (typeof data.goalId === "string")
372
596
  push(`${data.outcome === "blocked" ? tuiTheme.error("goal blocked") : tuiTheme.success("goal saved")} ${tuiTheme.muted(`${String(data.outcome).replaceAll("_", " ")} · record r${String(data.recordRevision)}`)}`);
373
- else {
374
- const results = Array.isArray(data.results) ? data.results : [];
375
- for (const result of results)
376
- if (typeof result === "string")
377
- push(`${tuiTheme.success("done")} ${result}`);
378
- }
379
597
  }
380
598
  else if (record.type === "ceo.human_requested") {
381
599
  push(`${tuiTheme.warning("needs you")} ${safeError(compact(record.data ?? {}))}`);
382
600
  }
383
- else if (record.type === "wake.abnormal_reason") {
384
- setThinking(false);
385
- push(`${tuiTheme.error("error")} ${safeError(typeof data.reason === "string" ? data.reason : "Wake failed")}\n`);
601
+ else if (record.type === "transcript.interrupted") {
602
+ updateThinking({ phase: "clear", text: "" });
603
+ clearLive();
604
+ push(`${tuiTheme.error("error")} ${safeError(typeof data.reason === "string" ? data.reason : "Turn failed")}\n`);
386
605
  }
387
606
  }
388
607
  function toolDetail(value) {
@@ -407,7 +626,7 @@ async function printStatus(stateDir, push) {
407
626
  const roots = Array.isArray(value.roots) ? value.roots : [];
408
627
  const team = Array.isArray(value.team) ? value.team : [];
409
628
  const pending = Array.isArray(value.pendingHuman) ? value.pendingHuman.length : 0;
410
- push([tuiTheme.strong("Status"), roots[0] ? ` ${tuiTheme.active(" GOAL ")} ${String(roots[0].objective)} ${tuiTheme.muted(String(roots[0].phase))}` : ` ${tuiTheme.muted("No active Goal")}`, ` ${tuiTheme.muted(team.length ? team.map((member) => `${String(member.agent)} ${String(member.status)}`).join(" · ") : "No Goal Agents")}`, ...(pending ? [` ${tuiTheme.warning(`${pending} decision${pending === 1 ? "" : "s"} need you`)}`] : []), ""].join("\n"));
629
+ push([tuiTheme.strong("Status"), roots[0] ? ` ${tuiTheme.active(" GOAL ")} ${String(roots[0].objective)} ${tuiTheme.muted(String(roots[0].phase))}` : ` ${tuiTheme.muted("No active Goal")}`, ` ${tuiTheme.muted(team.length ? team.map((member) => `${String(member.agent)} ${String(member.motion)}${member.lastOutcome ? `/${String(member.lastOutcome)}` : ""}`).join(" · ") : "No Goal Agents")}`, ...(pending ? [` ${tuiTheme.warning(`${pending} decision${pending === 1 ? "" : "s"} need you`)}`] : []), ""].join("\n"));
411
630
  }
412
631
  catch (error) {
413
632
  push(errorLine(error));
@@ -447,8 +666,15 @@ async function printRecords(text, stateDir, push) {
447
666
  }
448
667
  async function stopCeoWake(stateDir, push) {
449
668
  try {
450
- const wake = await requestControl(stateDir, { op: "wake.stop", agent: "ceo" });
451
- push(wake ? `${tuiTheme.warning("stopping")} Current run will close as interrupted.` : tuiTheme.muted("No active run."));
669
+ const snapshot = await requestControl(stateDir, { op: "status" });
670
+ const turns = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && Array.isArray(snapshot.turns) ? snapshot.turns : [];
671
+ const turnId = findLiveTurnId(turns);
672
+ if (!turnId) {
673
+ push(tuiTheme.muted("No active Turn."));
674
+ return;
675
+ }
676
+ await requestControl(stateDir, { op: "turn.interrupt", turnId });
677
+ push(`${tuiTheme.warning("stopping")} Current Turn was interrupted.`);
452
678
  }
453
679
  catch (error) {
454
680
  push(errorLine(error));
@@ -480,25 +706,6 @@ async function slashGoal(text, stateDir, push) {
480
706
  push(errorLine(error));
481
707
  }
482
708
  }
483
- /** /approve ID --reason TEXT --evidence SEQ[,SEQ] (and /reject with the same shape). */
484
- async function slashApprove(text, stateDir, push) {
485
- const approve = text.startsWith("/approve ");
486
- const rest = text.slice(approve ? 9 : 8).match(/^(\S+)(?:\s+--reason\s+(.*?))?(?:\s+--evidence\s+([\d,]+))?$/);
487
- if (!rest) {
488
- push(errorLine("usage: /approve ACTION_ID --reason TEXT --evidence SEQ[,SEQ]"));
489
- return;
490
- }
491
- const [, id = "", reason = "terminal approval", evidence = ""] = rest;
492
- try {
493
- const op = approve ? { op: "action.approve", id, reason, evidence: evidence ? evidence.split(",").map(Number) : [] }
494
- : { op: "action.reject", id, reason, evidence: evidence ? evidence.split(",").map(Number) : [] };
495
- await requestControl(stateDir, op);
496
- push(`${tuiTheme.success(approve ? "approved" : "rejected")} ${id}`);
497
- }
498
- catch (error) {
499
- push(errorLine(error));
500
- }
501
- }
502
709
  async function ensureDaemon(configPath, stateDir) {
503
710
  if (await controlAvailable(stateDir)) {
504
711
  const version = await requestControl(stateDir, { op: "daemon.version" }).catch(() => null);
@@ -544,7 +751,7 @@ function safeError(value) {
544
751
  const named = sanitized.match(/^(?:TypeError|RangeError|ReferenceError|SyntaxError|Error):\s*[^\n]+/m)?.[0];
545
752
  if (named)
546
753
  return named;
547
- return sanitized.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("file://") && !line.startsWith("at ") && line !== "^") ?? "Wake failed";
754
+ return sanitized.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("file://") && !line.startsWith("at ") && line !== "^") ?? "Turn failed";
548
755
  }
549
756
  function messageText(value) {
550
757
  if (typeof value === "string")
@@ -570,14 +777,8 @@ async function switchModelCommand(text, configPath, stateDir, push) {
570
777
  push(errorLine(error));
571
778
  }
572
779
  }
573
- /** /setup — re-enter the wizard, then reload the daemon so changes apply to the next wake. */
574
- async function runSetupReload(configPath, stateDir, push) {
575
- const result = await runSetupWizard();
576
- if (!result.profile) {
577
- push("setup cancelled; nothing changed");
578
- return;
579
- }
580
- applyWizardResult(result, configPath);
581
- push(await reloadDaemon(stateDir, configPath) ? "config reloaded — applies to the next wake" : `saved to ${resolve(configPath)}; daemon will pick it up on next start`);
780
+ function configuredRunnerProfile(configPath) {
781
+ const workspace = existsSync(configPath) ? loadConfig(configPath) : null;
782
+ return workspace?.runnerProfiles?.find((profile) => profile.id === "default") ?? workspace?.runnerProfiles?.[0] ?? readDefaultRunnerProfile();
582
783
  }
583
784
  //# sourceMappingURL=tui.js.map