@ordewell/cli 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AUTHORS.md CHANGED
@@ -27,6 +27,10 @@ record of who built what, rather than `git log`.
27
27
  - [@haltingstate](https://github.com/haltingstate) — made the terminal UI's
28
28
  stepper say what a saved step does next, instead of encoding it in a
29
29
  sentinel index (#123).
30
+ - [@Logic769](https://github.com/Logic769) — made `/resume` an alias of
31
+ `/sessions` in the planner chat (#139).
32
+ - Hanbin Tan ([@hskl18](https://github.com/hskl18)) — documented how
33
+ planners and runners get their models and credentials (#140).
30
34
 
31
35
  Thank you. Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
32
36
 
package/README.md CHANGED
@@ -6,7 +6,7 @@ and model assigned to each AI task. With worktree isolation enabled, code tasks
6
6
  run in separate worktrees; ops and script tasks run at the workspace root.
7
7
  Ordewell records task outcomes and combines code changes for review and merging.
8
8
 
9
- This README describes 0.11.1. See the [published releases](https://github.com/ordewell/ordewell/releases)
9
+ This README describes 0.11.2. See the [published releases](https://github.com/ordewell/ordewell/releases)
10
10
  for versions available to install and their release notes.
11
11
 
12
12
  ## Installation
package/dist/main.js CHANGED
@@ -188,7 +188,7 @@ Planner, models and runners:
188
188
  ordewell trailer [on|off] (headless) End Ordewell's commits with "Planned-with: Ordewell"; set only here or in settings.json
189
189
  ordewell autorun [on|off] (headless) Start each plan the planner submits, without review; /settings covers it
190
190
  ordewell parallel [<n>] (headless) How many AI tasks run at once, a live run included; /settings covers it
191
- ordewell refresh Re-discover runners and model catalogs
191
+ ordewell refresh Re-check runners; reread settings and cached models
192
192
  ordewell models List every provider's catalog (works without a server)
193
193
 
194
194
  Other:
@@ -792,7 +792,7 @@ var ApiClient = class _ApiClient {
792
792
  }
793
793
  /** The full provider catalog the daemon has discovered (used by the TUI model picker). */
794
794
  getModels() {
795
- return this.call("GET", "/api/models", "Failed to fetch models");
795
+ return this.call("GET", `/api/models?workspace=${encodeURIComponent(this.workspace)}`, "Failed to fetch models");
796
796
  }
797
797
  setRunnerEnabled(runner, enabled) {
798
798
  return this.call("PUT", `/api/runners/${runner}`, `Failed to ${enabled ? "enable" : "disable"} ${runner}`, { enabled });
@@ -1140,16 +1140,19 @@ var ApiClient = class _ApiClient {
1140
1140
  socket.close();
1141
1141
  } };
1142
1142
  }
1143
+ // Every settings call names this workspace, as `getModels` does: the
1144
+ // planner's model is judged against its runner catalog, which its own
1145
+ // CLAUDE_CONFIG_DIR can make another account's than the daemon's cwd.
1143
1146
  getSettings(sessionId) {
1144
- const route = sessionId ? `/api/settings?sessionId=${encodeURIComponent(sessionId)}` : "/api/settings";
1145
- return this.call("GET", route, "Failed to fetch settings");
1147
+ const session = sessionId ? `sessionId=${encodeURIComponent(sessionId)}&` : "";
1148
+ return this.call("GET", `/api/settings?${session}workspace=${encodeURIComponent(this.workspace)}`, "Failed to fetch settings");
1146
1149
  }
1147
1150
  /** Save run settings (ADR-0030); with `sessionId`, that session's run takes them at once. */
1148
1151
  updateRunSettings(changes, sessionId) {
1149
- return this.call("PATCH", "/api/settings", "Change run settings failed", { run: changes, ...sessionId ? { sessionId } : {} });
1152
+ return this.call("PATCH", "/api/settings", "Change run settings failed", { run: changes, ...sessionId ? { sessionId } : {}, workspace: this.workspace });
1150
1153
  }
1151
1154
  updateSettings(changes) {
1152
- return this.call("PATCH", "/api/settings", "Failed to update settings", changes);
1155
+ return this.call("PATCH", "/api/settings", "Failed to update settings", { ...changes, workspace: this.workspace });
1153
1156
  }
1154
1157
  };
1155
1158
 
@@ -4995,6 +4998,7 @@ function initialState(overrides = {}) {
4995
4998
  runSettings: { ...import_core31.RUN_SETTINGS_DEFAULTS },
4996
4999
  runSettingsKnown: false,
4997
5000
  settingsRead: 0,
5001
+ runSettingsRead: 0,
4998
5002
  plannerKnown: false,
4999
5003
  runnersKnown: false,
5000
5004
  providersKnown: false,
@@ -5283,7 +5287,7 @@ var SLASH_COMMANDS = [
5283
5287
  { name: "runners", usage: "/runners [<id> on|off]", description: `Enable or disable runners (${runnerIdList()})`, category: "models" },
5284
5288
  { name: "login", usage: "/login [<runner>]", description: "Sign the active coding agent in, or the one named, in this terminal", category: "models" },
5285
5289
  { name: "settings", usage: "/settings", description: "Run settings: autonomy, supervisor, merging, autorun and parallel tasks", category: "models" },
5286
- { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
5290
+ { name: "refresh", usage: "/refresh", description: "Re-check runners; reread settings and cached models", category: "models" },
5287
5291
  // Sessions
5288
5292
  { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
5289
5293
  { name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
@@ -5298,7 +5302,27 @@ var SLASH_COMMANDS = [
5298
5302
  { name: "mouse", usage: "/mouse [on|off]", description: "Wheel scrolling and drag-select, per-pane in the body and full-width in the composer (on), or the terminal's own selection (off)", category: "system" },
5299
5303
  { name: "quit", usage: "/quit", description: "Exit the TUI", category: "system" }
5300
5304
  ];
5301
- var BY_NAME = new Map(SLASH_COMMANDS.map((c) => [c.name, c]));
5305
+ var COMMAND_ALIASES = [
5306
+ // Editors call reopening a saved session "resume"; here it must open the same
5307
+ // saved-session picker as /sessions, not restart a task or a runner.
5308
+ { name: "resume", target: "sessions", description: "Alias for /sessions: open a saved session, not restart a task" }
5309
+ ];
5310
+ function slashCommandTable() {
5311
+ const table2 = [];
5312
+ for (const command of SLASH_COMMANDS) {
5313
+ table2.push(command);
5314
+ for (const alias of COMMAND_ALIASES) {
5315
+ if (alias.target !== command.name) continue;
5316
+ table2.push({ name: alias.name, usage: `/${alias.name}`, description: alias.description, category: command.category });
5317
+ }
5318
+ }
5319
+ return table2;
5320
+ }
5321
+ var BY_NAME = new Map(slashCommandTable().map((c) => [c.name, c]));
5322
+ function canonicalCommandName(name) {
5323
+ const key = name.replace(/^\//, "").toLowerCase();
5324
+ return COMMAND_ALIASES.find((a) => a.name === key)?.target ?? key;
5325
+ }
5302
5326
  var skillCommands = [];
5303
5327
  var skillByName = /* @__PURE__ */ new Map();
5304
5328
  function registerSkillCommands(skills) {
@@ -5369,7 +5393,7 @@ function composerCompletions(token, runner) {
5369
5393
  return tokenCompletions(token).map((c) => c.name);
5370
5394
  }
5371
5395
  function tokenCompletions(token) {
5372
- const pool = token.start === 0 ? [...SLASH_COMMANDS, ...skillCommands] : skillCommands;
5396
+ const pool = token.start === 0 ? [...slashCommandTable(), ...skillCommands] : skillCommands;
5373
5397
  return pool.filter((c) => c.name.startsWith(token.name));
5374
5398
  }
5375
5399
 
@@ -5929,6 +5953,8 @@ function chatActivity(state) {
5929
5953
  const own = findTask(state.tasks, tv.taskId);
5930
5954
  const task = state.tasks.find((t) => t.subtasks?.some((s) => s.id === tv.taskId)) ?? own;
5931
5955
  if (!task) return null;
5956
+ const attaching = (0, import_core38.attachingLabel)(task);
5957
+ if (attaching) return { label: `${attaching}\u2026`, busy: true };
5932
5958
  if ((0, import_core31.isTaskRunning)(task) || own && (0, import_core31.isTaskRunning)(own)) return { label: "Working\u2026", busy: true };
5933
5959
  const at = task.status === "awaiting_user" && task.awaitingReason === "usage-limit" && task.resumeAt ? new Date(task.resumeAt) : null;
5934
5960
  if (at && !Number.isNaN(at.getTime())) return { label: `paused \xB7 resumes ${(0, import_core38.resumeTime)(at)}`, busy: false };
@@ -6404,7 +6430,7 @@ function taskLines(state, planRow, index, cols) {
6404
6430
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
6405
6431
  const runner = row.runner ?? "";
6406
6432
  const bodyPad = parent ? " " : " ";
6407
- const activity = row.approvals ? `${inRow(row.approvals)}: enter opens it` : row.awaiting ? inRow(row.awaiting) : row.receipt ? inRow(row.receipt) : "";
6433
+ const activity = row.approvals ? `${inRow(row.approvals)}: enter opens it` : row.awaiting ? inRow(row.awaiting) : row.receipt ? inRow(row.receipt) : row.attaching ? inRow(row.attaching) : "";
6408
6434
  const skills = (task.skills ?? []).join(" \xB7 ");
6409
6435
  const meta = [activity, ...row.research && row.running ? [style.cyan("research")] : [], runner, model];
6410
6436
  const variant = task.type === "ai" ? sanitize(task.assignedModel?.thinkingEffort ?? "") : "";
@@ -6570,7 +6596,7 @@ function keySheetBody(cols) {
6570
6596
  function commandSheetBody(cols) {
6571
6597
  const body = [];
6572
6598
  for (const [category, title] of Object.entries(CATEGORY_TITLE)) {
6573
- const commands = SLASH_COMMANDS.filter((c) => c.category === category);
6599
+ const commands = slashCommandTable().filter((c) => c.category === category);
6574
6600
  if (commands.length === 0) continue;
6575
6601
  body.push(style.bold(title));
6576
6602
  for (const command of commands) {
@@ -8087,7 +8113,8 @@ function stopPlanning(state, sessionId) {
8087
8113
  function queueToEditor(state) {
8088
8114
  const all = (0, import_core47.unsendAll)(state.queuedPrompts);
8089
8115
  if (!all) return state;
8090
- return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
8116
+ const text = (0, import_core47.aheadOfDraft)(all.text, state.editor.text);
8117
+ return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text, cursor: all.text.length } };
8091
8118
  }
8092
8119
  function unsendLatest(state) {
8093
8120
  const latest = (0, import_core47.unsendLatest)(state.queuedPrompts);
@@ -8168,6 +8195,7 @@ function runTaskToView(t) {
8168
8195
  status: planLabel(t.status, "pending"),
8169
8196
  idleSince: t.idleSince ?? null,
8170
8197
  ...t.completionReceived === "done" || t.completionReceived === "failed" || t.completionReceived === "blocked" ? { completionReceived: t.completionReceived } : {},
8198
+ ...t.attaching === 1 || t.attaching === 2 ? { attaching: t.attaching } : {},
8171
8199
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
8172
8200
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
8173
8201
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -8390,8 +8418,10 @@ function saveSettings(state, effect) {
8390
8418
 
8391
8419
  // src/tui/reducers/commands.ts
8392
8420
  var unknownCommand = (state, name) => fail3(state, `Unknown command: /${name}. Type /help to see what's available.`);
8393
- function runCommand(state, { name, args }) {
8394
- if (!findCommand(name)) return unknownCommand(state, name);
8421
+ function runCommand(state, parsed) {
8422
+ const name = canonicalCommandName(parsed.name);
8423
+ const { args } = parsed;
8424
+ if (!findCommand(parsed.name)) return unknownCommand(state, parsed.name);
8395
8425
  switch (name) {
8396
8426
  case "help":
8397
8427
  return step({ ...state, overlay: { kind: "help", scroll: 0 } });
@@ -8556,6 +8586,8 @@ function newSession(state) {
8556
8586
  planScroll: null,
8557
8587
  status: "idle",
8558
8588
  planner: "idle",
8589
+ plannerPause: null,
8590
+ plannerResend: null,
8559
8591
  // Prompts belong to the session that raised them; the old planner is gone
8560
8592
  // and its pending requests deny on their own timeout.
8561
8593
  pendingApprovals: [],
@@ -9019,6 +9051,8 @@ function reduceAction(state, action) {
9019
9051
  return step(withRun({ ...state, sessionId: action.sessionId, goal: action.goal, accountLimits: [], servedModels: {}, taskView: null, status: "idle" }, import_core52.EMPTY_RUN));
9020
9052
  // A loaded session is a different one: the turn the old one had out, the
9021
9053
  // prompts held behind it and a stop armed against it are not this one's.
9054
+ // Nor is an open prompt editor: task ids repeat across plans, so the
9055
+ // loaded plan's `task-1` would keep it, and Enter save the old draft there.
9022
9056
  // (`sessionStarted` cannot reset them: `startConversation` dispatches it
9023
9057
  // as its own turn begins.)
9024
9058
  case "sessionLoaded":
@@ -9029,12 +9063,17 @@ function reduceAction(state, action) {
9029
9063
  sessionId: action.sessionId,
9030
9064
  goal: action.goal,
9031
9065
  taskView: null,
9066
+ expandedTaskId: null,
9067
+ taskEditor: null,
9032
9068
  status: "idle",
9033
9069
  planner: "idle",
9070
+ plannerPause: null,
9071
+ plannerResend: null,
9034
9072
  turnGate: import_core52.NO_TURN,
9035
9073
  pendingApprovals: [],
9036
9074
  queuedPrompts: import_core52.EMPTY_HOLD,
9037
9075
  stopArmed: false,
9076
+ stopRequested: false,
9038
9077
  forceSendPending: null,
9039
9078
  overlay: state.overlay?.kind === "approval" ? null : state.overlay,
9040
9079
  runSettingsKnown: false
@@ -9045,21 +9084,25 @@ function reduceAction(state, action) {
9045
9084
  // id is let go rather than guessed at. A reply that was merely lost is not
9046
9085
  // this: the session is the daemon's and the turn may be running (`turnLost`).
9047
9086
  case "sessionCleared":
9087
+ if (stale(state, action.sessionId)) return step(state);
9048
9088
  return step(withRun({
9049
- ...state,
9089
+ ...queueToEditor(state),
9050
9090
  accountLimits: [],
9051
9091
  servedModels: {},
9052
9092
  sessionId: null,
9053
9093
  goal: "",
9054
9094
  taskView: null,
9095
+ expandedTaskId: null,
9096
+ taskEditor: null,
9055
9097
  status: "idle",
9098
+ plannerPause: null,
9099
+ plannerResend: null,
9056
9100
  turnGate: import_core52.NO_TURN,
9057
9101
  pendingApprovals: [],
9058
- queuedPrompts: import_core52.EMPTY_HOLD,
9059
9102
  stopArmed: false,
9060
9103
  forceSendPending: null,
9061
9104
  overlay: state.overlay?.kind === "approval" ? null : state.overlay,
9062
- runSettingsKnown: false
9105
+ runSettingsKnown: action.unreached ? state.runSettingsKnown : false
9063
9106
  }, import_core52.EMPTY_RUN));
9064
9107
  // Deleting the session on screen stops it in the daemon; a view left on it
9065
9108
  // would answer every next message with a session-not-found.
@@ -9262,12 +9305,16 @@ function reduceAction(state, action) {
9262
9305
  });
9263
9306
  }
9264
9307
  case "settingsLoaded": {
9265
- if (action.read !== void 0 && action.read < state.settingsRead) return step(state);
9266
- const forThisSession = action.sessionId === void 0 ? action.read === void 0 || state.sessionId === null : action.sessionId === state.sessionId;
9267
- const next = applySettings(state, action.settings, forThisSession);
9308
+ const { read } = action;
9309
+ const forThisSession = action.sessionId === void 0 ? read === void 0 || state.sessionId === null : action.sessionId === state.sessionId;
9310
+ const workspaceFresh = read === void 0 || read >= state.settingsRead;
9311
+ const runFresh = forThisSession && (read === void 0 || read >= state.runSettingsRead);
9312
+ if (!workspaceFresh && !runFresh) return step(state);
9313
+ const next = workspaceFresh ? applySettings(state, action.settings, runFresh) : applySettings(state, { run: action.settings.run }, true);
9268
9314
  return step(refreshStepper({
9269
9315
  ...next,
9270
- settingsRead: action.read !== void 0 && forThisSession ? Math.max(state.settingsRead, action.read) : state.settingsRead
9316
+ settingsRead: read !== void 0 && workspaceFresh ? Math.max(state.settingsRead, read) : state.settingsRead,
9317
+ runSettingsRead: read !== void 0 && runFresh ? Math.max(state.runSettingsRead, read) : state.runSettingsRead
9271
9318
  }));
9272
9319
  }
9273
9320
  case "modelsLoaded":
@@ -9293,13 +9340,17 @@ function reduceAction(state, action) {
9293
9340
  return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
9294
9341
  // A fork holds no run and no turn, whatever the session it came from was
9295
9342
  // doing — the original keeps both, and its events are stale from here on.
9296
- // Its plan arrives next, onto an empty run view.
9343
+ // Its plan arrives next, onto an empty run view. Nor does it keep an open
9344
+ // prompt editor: the fork copies the task ids, so Enter would save the
9345
+ // original's draft into the fork.
9297
9346
  case "sessionForked":
9298
9347
  return step(withRun({
9299
9348
  ...state,
9300
9349
  sessionId: action.sessionId,
9301
9350
  goal: action.goal,
9302
9351
  taskView: null,
9352
+ expandedTaskId: null,
9353
+ taskEditor: null,
9303
9354
  status: "idle",
9304
9355
  planner: "idle",
9305
9356
  turnGate: import_core52.NO_TURN,
@@ -9307,6 +9358,7 @@ function reduceAction(state, action) {
9307
9358
  pendingApprovals: [],
9308
9359
  queuedPrompts: import_core52.EMPTY_HOLD,
9309
9360
  stopArmed: false,
9361
+ stopRequested: false,
9310
9362
  forceSendPending: null,
9311
9363
  overlay: state.overlay?.kind === "approval" ? null : state.overlay,
9312
9364
  runSettingsKnown: false
@@ -9347,6 +9399,7 @@ function reduceAction(state, action) {
9347
9399
  case "daemonReconnected":
9348
9400
  return step(state, [{ type: "loadIsolationPreview" }]);
9349
9401
  case "failed": {
9402
+ if (state.sessionId !== null && stale(state, action.sessionId)) return step(state);
9350
9403
  if (!action.turn) {
9351
9404
  const abandoned = reportError(taskLogAbandoned(state), action.message);
9352
9405
  return step(state.status === "starting" ? { ...abandoned, status: "idle" } : abandoned);
@@ -9367,7 +9420,10 @@ function reduceAction(state, action) {
9367
9420
  if (!plannerInFlight(state)) return step(reported);
9368
9421
  return drainQueue(state, reported);
9369
9422
  }
9423
+ // As `failed`: an opening that let itself go asks on the welcome it left,
9424
+ // never over a session loaded meanwhile.
9370
9425
  case "workspaceNeedsInit":
9426
+ if (state.sessionId !== null && stale(state, action.sessionId)) return step(state);
9371
9427
  return step({
9372
9428
  ...state,
9373
9429
  planner: "idle",
@@ -9378,7 +9434,23 @@ function reduceAction(state, action) {
9378
9434
  action: { kind: "init-workspace", goal: action.goal, workspace: action.workspace }
9379
9435
  }
9380
9436
  });
9437
+ // A refused opening stays on screen while the daemon is revived; the
9438
+ // user may have moved to another session or a fresh welcome since, and
9439
+ // that view, its queue and its planner are not the opening's to take.
9440
+ case "openingRevived": {
9441
+ const restarted = "The server had stopped; started a new one";
9442
+ if (stale(state, action.sessionId)) {
9443
+ return step(say(state, "system", `${restarted}. "${action.goal}" was not sent again, as it is no longer the session on screen.`));
9444
+ }
9445
+ if (!plannerInFlight(state) || state.stopRequested) {
9446
+ const cleared = reduceAction(state, { type: "sessionCleared", sessionId: action.sessionId, unreached: true }).state;
9447
+ return step(say({ ...cleared, planner: "idle", stopRequested: false }, "system", `${restarted}. The goal was not sent again, as planning was stopped.`));
9448
+ }
9449
+ const retry = { type: "startConversation", goal: action.goal, sessionId: action.sessionId, ...action.allowInit ? { allowInit: true } : {} };
9450
+ return step(say(state, "system", `${restarted} and retried.`), [retry]);
9451
+ }
9381
9452
  case "notice":
9453
+ if (stale(state, action.sessionId)) return step(state);
9382
9454
  return step(say(state, action.level === "error" ? "error" : "system", action.message, !action.holdPlace));
9383
9455
  case "resize": {
9384
9456
  const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
@@ -10024,23 +10096,40 @@ function dispatchLifecycle(dispatch, event, sessionId) {
10024
10096
 
10025
10097
  // src/tui/effects.ts
10026
10098
  async function runEffect(effect, deps) {
10099
+ const performed = effect.type === "startConversation" ? { ...effect, sessionId: effect.sessionId ?? deps.newSessionId() } : effect;
10100
+ const opening = performed.type === "startConversation" ? performed : null;
10027
10101
  const turn = isPlannerTurn(effect);
10028
- const fail4 = (action) => deps.dispatch(turn ? { ...action, turn } : action);
10102
+ const turnSession = turn && "sessionId" in performed ? { sessionId: performed.sessionId } : {};
10103
+ const fail4 = (action) => deps.dispatch(turn ? { ...action, turn, ...turnSession } : action);
10029
10104
  try {
10030
- await perform(effect, deps);
10105
+ await perform(performed, deps);
10031
10106
  } catch (err) {
10032
10107
  if (!isConnectionRefused(err)) {
10033
10108
  fail4(failedFrom(err));
10034
10109
  return;
10035
10110
  }
10036
10111
  let revived = false;
10037
- try {
10038
- revived = await deps.reviveDaemon();
10039
- } catch {
10040
- revived = false;
10112
+ if (effect.type !== "startConversation" || effect.sessionId === void 0) {
10113
+ try {
10114
+ revived = await deps.reviveDaemon();
10115
+ } catch {
10116
+ revived = false;
10117
+ }
10041
10118
  }
10042
10119
  if (!revived) {
10120
+ if (opening) deps.dispatch({ type: "sessionCleared", sessionId: opening.sessionId, unreached: true });
10043
10121
  fail4({ type: "failed", message: describeConnectionRefused(deps.port) });
10122
+ if (opening) await reloadSettings(deps);
10123
+ return;
10124
+ }
10125
+ if (opening) {
10126
+ deps.dispatch({ type: "daemonReconnected" });
10127
+ deps.dispatch({
10128
+ type: "openingRevived",
10129
+ sessionId: opening.sessionId,
10130
+ goal: opening.goal,
10131
+ ...opening.allowInit ? { allowInit: opening.allowInit } : {}
10132
+ });
10044
10133
  return;
10045
10134
  }
10046
10135
  deps.dispatch({ type: "notice", message: "The server had stopped; started a new one and retried." });
@@ -10048,7 +10137,7 @@ async function runEffect(effect, deps) {
10048
10137
  deps.dispatch({ type: "daemonReconnected" });
10049
10138
  }
10050
10139
  try {
10051
- await perform(effect, deps);
10140
+ await perform(performed, deps);
10052
10141
  } catch (retryErr) {
10053
10142
  fail4(isConnectionRefused(retryErr) ? { type: "failed", message: describeConnectionRefused(deps.port) } : failedFrom(retryErr));
10054
10143
  }
@@ -10084,7 +10173,7 @@ async function perform(effect, deps) {
10084
10173
  const { api, dispatch, workspace } = deps;
10085
10174
  switch (effect.type) {
10086
10175
  case "startConversation": {
10087
- const sessionId = deps.newSessionId();
10176
+ const { sessionId } = effect;
10088
10177
  dispatch({ type: "sessionStarted", sessionId, goal: effect.goal });
10089
10178
  try {
10090
10179
  await converse(deps, sessionId, () => api.startConversation(sessionId, effect.goal, void 0, workspace, effect.allowInit), true);
@@ -10093,11 +10182,15 @@ async function perform(effect, deps) {
10093
10182
  await reloadSettings(deps, sessionId);
10094
10183
  throw err;
10095
10184
  }
10185
+ if (isConnectionRefused(err)) {
10186
+ unfollow(deps, sessionId);
10187
+ throw err;
10188
+ }
10096
10189
  unfollow(deps, sessionId);
10097
- dispatch({ type: "sessionCleared" });
10190
+ dispatch({ type: "sessionCleared", sessionId });
10098
10191
  await reloadSettings(deps);
10099
10192
  if (err instanceof WorkspaceInitNeededError && !effect.allowInit) {
10100
- dispatch({ type: "workspaceNeedsInit", goal: effect.goal, workspace: err.workspace });
10193
+ dispatch({ type: "workspaceNeedsInit", goal: effect.goal, workspace: err.workspace, sessionId });
10101
10194
  return;
10102
10195
  }
10103
10196
  throw err;
@@ -10167,7 +10260,7 @@ async function perform(effect, deps) {
10167
10260
  try {
10168
10261
  await api.respondToApproval(effect.sessionId, effect.approvalId, effect.granted);
10169
10262
  } catch {
10170
- dispatch({ type: "failed", message: "Could not deliver the approval answer: the planner will treat it as denied." });
10263
+ dispatch({ type: "failed", message: "Could not deliver the approval answer: the planner will treat it as denied.", sessionId: effect.sessionId });
10171
10264
  }
10172
10265
  return;
10173
10266
  }
@@ -10176,7 +10269,7 @@ async function perform(effect, deps) {
10176
10269
  try {
10177
10270
  await api.respondToApproval(effect.sessionId, effect.approvalId, effect.answer);
10178
10271
  } catch {
10179
- dispatch({ type: "failed", message: "Could not deliver the answer: the task is still waiting for approval." });
10272
+ dispatch({ type: "failed", message: "Could not deliver the answer: the task is still waiting for approval.", sessionId: effect.sessionId });
10180
10273
  }
10181
10274
  return;
10182
10275
  }
@@ -10693,7 +10786,7 @@ async function converse(deps, sessionId, call, opening = false) {
10693
10786
  }
10694
10787
  if (unusable) {
10695
10788
  const { err, shown } = unusable;
10696
- deps.dispatch({ ...failedFrom(shown ? new Error(`The planner's reply arrived but could not be shown: ${explain(err)}`) : err), turn: true });
10789
+ deps.dispatch({ ...failedFrom(shown ? new Error(`The planner's reply arrived but could not be shown: ${explain(err)}`) : err), turn: true, sessionId });
10697
10790
  } else if (lost !== null) {
10698
10791
  await settleLostReply(deps, sessionId, { opening, reason: lost });
10699
10792
  }
@@ -10737,15 +10830,15 @@ async function readBackSession(deps, sessionId, how) {
10737
10830
  } catch (err) {
10738
10831
  if (err instanceof DaemonError && err.code === "session_not_found" && how.opening) {
10739
10832
  unfollow(deps, sessionId);
10740
- deps.dispatch({ type: "sessionCleared" });
10741
- deps.dispatch({ type: "failed", message: "The reply was lost and the server holds no session for it. Send your goal again to start over.", code: err.code, turn: true });
10833
+ deps.dispatch({ type: "sessionCleared", sessionId });
10834
+ deps.dispatch({ type: "failed", message: "The reply was lost and the server holds no session for it. Send your goal again to start over.", code: err.code, turn: true, sessionId });
10742
10835
  return;
10743
10836
  }
10744
10837
  if (how.turnEnded || err instanceof DaemonError && err.code === "session_not_found") {
10745
- deps.dispatch({ ...failedFrom(err), turn: true });
10838
+ deps.dispatch({ ...failedFrom(err), turn: true, sessionId });
10746
10839
  return;
10747
10840
  }
10748
- deps.dispatch({ type: "notice", level: "error", message: `Could not read the session back from the server: ${explain(err)}` });
10841
+ deps.dispatch({ type: "notice", level: "error", message: `Could not read the session back from the server: ${explain(err)}`, sessionId });
10749
10842
  }
10750
10843
  }
10751
10844
  function restoredChat(plan, sessionId) {
@@ -11208,6 +11301,7 @@ var writeLine = (line) => console.error(line);
11208
11301
  var toStderr = { onNotice: writeLine, onUpdate: writeLine };
11209
11302
 
11210
11303
  // src/tui/index.ts
11304
+ var DAEMON_STOP_GRACE_MS = 3e3;
11211
11305
  async function runLogin(start, command, cwd, env) {
11212
11306
  const plan = await (0, import_core55.planDirectLaunch)(command[0], command.slice(1));
11213
11307
  return new Promise((resolve6, reject) => {
@@ -11353,12 +11447,19 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
11353
11447
  const { rows, cols } = terminal.size();
11354
11448
  app.dispatch({ type: "resize", rows, cols });
11355
11449
  app.start();
11450
+ let exiting = false;
11356
11451
  function shutdown(code) {
11452
+ if (exiting) return process.exit(code);
11453
+ exiting = true;
11357
11454
  watchingSettings.dispose();
11358
11455
  terminal?.close();
11359
- if (owned) stopDaemon(port).catch(() => {
11456
+ if (!owned) return process.exit(code);
11457
+ const grace = setTimeout(() => process.exit(code), DAEMON_STOP_GRACE_MS);
11458
+ void stopDaemon(port).catch(() => {
11459
+ }).finally(() => {
11460
+ clearTimeout(grace);
11461
+ process.exit(code);
11360
11462
  });
11361
- process.exit(code);
11362
11463
  }
11363
11464
  process.on("SIGINT", () => {
11364
11465
  if (loginRunning) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -54,8 +54,8 @@
54
54
  "typecheck": "tsc --noEmit"
55
55
  },
56
56
  "dependencies": {
57
- "@ordewell/core": "^0.11.1",
58
- "@ordewell/web": "^0.11.1",
57
+ "@ordewell/core": "^0.11.2",
58
+ "@ordewell/web": "^0.11.2",
59
59
  "ws": "^8.18.0"
60
60
  },
61
61
  "devDependencies": {