@wrongstack/tui 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1151,18 +1151,18 @@ function PhasePanel({ phases, runningPhaseIds, nowTick }) {
1151
1151
  ] }),
1152
1152
  list.map((phase, i) => {
1153
1153
  const phaseKey = Object.keys(phases).find((k) => phases[k] === phase) ?? String(i);
1154
- const st = s(phase.status);
1154
+ const st2 = s(phase.status);
1155
1155
  const progress = phase.totalTasks > 0 ? `${phase.completedTasks}/${phase.totalTasks}` : "";
1156
1156
  const elapsed = phase.startedAt ? fmtElapsed3(nowTick - phase.startedAt) : "";
1157
1157
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1158
- /* @__PURE__ */ jsx(Text, { color: st.color, children: st.icon }),
1158
+ /* @__PURE__ */ jsx(Text, { color: st2.color, children: st2.icon }),
1159
1159
  /* @__PURE__ */ jsx(Text, { children: phase.name.slice(0, 14).padEnd(14) }),
1160
- /* @__PURE__ */ jsx(Text, { color: st.color, dimColor: true, children: st.icon === "\u25CF" ? phase.status : "" }),
1160
+ /* @__PURE__ */ jsx(Text, { color: st2.color, dimColor: true, children: st2.icon === "\u25CF" ? phase.status : "" }),
1161
1161
  progress ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1162
1162
  " ",
1163
1163
  progress
1164
1164
  ] }) : null,
1165
- elapsed && st.icon === "\u25CF" ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1165
+ elapsed && st2.icon === "\u25CF" ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1166
1166
  " ",
1167
1167
  elapsed
1168
1168
  ] }) : null
@@ -1172,6 +1172,204 @@ function PhasePanel({ phases, runningPhaseIds, nowTick }) {
1172
1172
  }
1173
1173
  );
1174
1174
  }
1175
+ var fmtElapsed4 = (ms) => {
1176
+ const s2 = Math.floor(ms / 1e3);
1177
+ const m = Math.floor(s2 / 60);
1178
+ const h = Math.floor(m / 60);
1179
+ if (h > 0) return `${h}:${String(m % 60).padStart(2, "0")}:${String(s2 % 60).padStart(2, "0")}`;
1180
+ if (m > 0) return `${m}:${String(s2 % 60).padStart(2, "0")}`;
1181
+ return `${s2}s`;
1182
+ };
1183
+ var STATUS4 = {
1184
+ allocating: { icon: "\u25CB", color: "gray" },
1185
+ active: { icon: "\u25CF", color: "yellow" },
1186
+ committing: { icon: "\u25D0", color: "cyan" },
1187
+ merging: { icon: "\u21E1", color: "blue" },
1188
+ merged: { icon: "\u2713", color: "green" },
1189
+ "needs-review": { icon: "\u26A0", color: "magenta" },
1190
+ failed: { icon: "\u2717", color: "red" }
1191
+ };
1192
+ function st(status) {
1193
+ return STATUS4[status] ?? { icon: "?", color: "white" };
1194
+ }
1195
+ function WorktreePanel({
1196
+ worktrees,
1197
+ nowTick
1198
+ }) {
1199
+ const list = Object.values(worktrees);
1200
+ if (list.length === 0) return null;
1201
+ const active = list.filter((w) => w.status === "active" || w.status === "committing" || w.status === "merging").length;
1202
+ const merged = list.filter((w) => w.status === "merged").length;
1203
+ const failed = list.filter((w) => w.status === "failed" || w.status === "needs-review").length;
1204
+ return /* @__PURE__ */ jsxs(
1205
+ Box,
1206
+ {
1207
+ flexDirection: "column",
1208
+ paddingX: 1,
1209
+ borderStyle: "single",
1210
+ borderTop: false,
1211
+ borderBottom: false,
1212
+ borderLeft: false,
1213
+ borderRight: false,
1214
+ children: [
1215
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1216
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Worktrees" }),
1217
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1218
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1219
+ "\u25B6",
1220
+ active
1221
+ ] }),
1222
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1223
+ "\u2713",
1224
+ merged
1225
+ ] }),
1226
+ failed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1227
+ "\u2717",
1228
+ failed
1229
+ ] }) : null,
1230
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1231
+ "\xB7 ",
1232
+ list.length,
1233
+ " total \xB7 Ctrl+W for details"
1234
+ ] })
1235
+ ] }),
1236
+ list.map((w) => {
1237
+ const s2 = st(w.status);
1238
+ const conflict = w.status === "needs-review";
1239
+ const elapsed = w.allocatedAt ? fmtElapsed4(nowTick - w.allocatedAt) : "";
1240
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1241
+ /* @__PURE__ */ jsx(Text, { color: s2.color, children: s2.icon }),
1242
+ /* @__PURE__ */ jsx(Text, { children: w.branch.replace(/^wstack\/ap\//, "").slice(0, 18).padEnd(18) }),
1243
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: w.ownerLabel.slice(0, 12) }),
1244
+ conflict ? /* @__PURE__ */ jsx(Text, { color: "magenta", children: " CONFLICT" }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1245
+ " +",
1246
+ w.insertions,
1247
+ "/-",
1248
+ w.deletions,
1249
+ " ",
1250
+ w.files,
1251
+ "f"
1252
+ ] }),
1253
+ elapsed && (w.status === "active" || w.status === "committing") ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1254
+ " ",
1255
+ elapsed
1256
+ ] }) : null
1257
+ ] }, w.branch);
1258
+ })
1259
+ ]
1260
+ }
1261
+ );
1262
+ }
1263
+ var fmtElapsed5 = (ms) => {
1264
+ const s2 = Math.floor(ms / 1e3);
1265
+ const m = Math.floor(s2 / 60);
1266
+ const h = Math.floor(m / 60);
1267
+ if (h > 0) return `${h}:${String(m % 60).padStart(2, "0")}:${String(s2 % 60).padStart(2, "0")}`;
1268
+ if (m > 0) return `${m}:${String(s2 % 60).padStart(2, "0")}`;
1269
+ return `${s2}s`;
1270
+ };
1271
+ var WT_STATUS = {
1272
+ allocating: { icon: "\u25CB", color: "gray", label: "allocating" },
1273
+ active: { icon: "\u25CF", color: "yellow", label: "active" },
1274
+ committing: { icon: "\u25D0", color: "cyan", label: "committing" },
1275
+ merging: { icon: "\u21E1", color: "blue", label: "merging" },
1276
+ merged: { icon: "\u2713", color: "green", label: "merged" },
1277
+ "needs-review": { icon: "\u26A0", color: "magenta", label: "needs-review" },
1278
+ failed: { icon: "\u2717", color: "red", label: "failed" }
1279
+ };
1280
+ function fmt(s2) {
1281
+ return WT_STATUS[s2] ?? { icon: "?", color: "white", label: s2 };
1282
+ }
1283
+ function WorktreeMonitor({
1284
+ worktrees,
1285
+ baseBranch,
1286
+ nowTick,
1287
+ onClose
1288
+ }) {
1289
+ useInput((_, key) => {
1290
+ if (key.escape) onClose();
1291
+ });
1292
+ const list = Object.values(worktrees);
1293
+ const active = list.filter((w) => ["active", "committing", "merging"].includes(w.status)).length;
1294
+ const merged = list.filter((w) => w.status === "merged").length;
1295
+ const failed = list.filter((w) => w.status === "failed" || w.status === "needs-review").length;
1296
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
1297
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginBottom: 1, children: [
1298
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "WORKTREE MONITOR" }),
1299
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1300
+ baseBranch ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1301
+ "base ",
1302
+ baseBranch
1303
+ ] }) : null,
1304
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1305
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1306
+ "\u25B6",
1307
+ active
1308
+ ] }),
1309
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1310
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1311
+ "\u2713",
1312
+ merged
1313
+ ] }),
1314
+ failed > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1315
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1316
+ /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1317
+ "\u2717",
1318
+ failed
1319
+ ] })
1320
+ ] }) : null,
1321
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502 Ctrl+W / Esc to close" })
1322
+ ] }),
1323
+ list.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No worktrees. They appear when AutoPhase runs with isolation on." }) : list.map((w) => {
1324
+ const s2 = fmt(w.status);
1325
+ const short = w.branch.replace(/^wstack\/ap\//, "");
1326
+ const elapsed = w.allocatedAt ? fmtElapsed5(nowTick - w.allocatedAt) : "\u2014";
1327
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1328
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1329
+ /* @__PURE__ */ jsx(Text, { color: s2.color, bold: true, children: s2.icon }),
1330
+ /* @__PURE__ */ jsx(Text, { bold: true, children: short }),
1331
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1332
+ /* @__PURE__ */ jsx(Text, { color: s2.color, children: s2.label }),
1333
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1334
+ "\xB7 elapsed ",
1335
+ elapsed
1336
+ ] })
1337
+ ] }),
1338
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginLeft: 2, children: [
1339
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1340
+ w.baseBranch ?? baseBranch ?? "base",
1341
+ " \u2192 ",
1342
+ short
1343
+ ] }),
1344
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1345
+ "\xB7 owner: ",
1346
+ w.ownerLabel
1347
+ ] })
1348
+ ] }),
1349
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginLeft: 2, children: [
1350
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1351
+ "+",
1352
+ w.insertions
1353
+ ] }),
1354
+ /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1355
+ "-",
1356
+ w.deletions
1357
+ ] }),
1358
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1359
+ "\xB7 ",
1360
+ w.files,
1361
+ " files"
1362
+ ] })
1363
+ ] }),
1364
+ w.conflictFiles && w.conflictFiles.length > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
1365
+ "conflicts: ",
1366
+ w.conflictFiles.join(", ")
1367
+ ] }) }) : null
1368
+ ] }, w.branch);
1369
+ }),
1370
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Esc close \xB7 merge conflicts with /worktree merge <branch>" }) })
1371
+ ] });
1372
+ }
1175
1373
 
1176
1374
  // src/markdown-table.ts
1177
1375
  function renderMarkdownTables(text, maxWidth) {
@@ -2350,7 +2548,7 @@ function Input({
2350
2548
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
2351
2549
  ] });
2352
2550
  }
2353
- function fmtElapsed4(ms) {
2551
+ function fmtElapsed6(ms) {
2354
2552
  if (ms < 1e3) return `${ms}ms`;
2355
2553
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2356
2554
  const m = Math.floor(ms / 6e4);
@@ -2365,7 +2563,7 @@ function fmtRecentTool2(tool) {
2365
2563
  const status = tool.ok === false ? "fail" : "ok";
2366
2564
  const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
2367
2565
  const parts = [status, name];
2368
- if (typeof tool.durationMs === "number") parts.push(fmtElapsed4(tool.durationMs));
2566
+ if (typeof tool.durationMs === "number") parts.push(fmtElapsed6(tool.durationMs));
2369
2567
  if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes3(tool.outputBytes));
2370
2568
  if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
2371
2569
  return parts.join(" ");
@@ -2386,7 +2584,7 @@ function LiveActivityStrip({
2386
2584
  running.map((e) => {
2387
2585
  const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
2388
2586
  const taskElapsed = now - e.startedAt;
2389
- const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed4(toolElapsed)})` : "idle between tools";
2587
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed6(toolElapsed)})` : "idle between tools";
2390
2588
  const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
2391
2589
  const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
2392
2590
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
@@ -2400,7 +2598,7 @@ function LiveActivityStrip({
2400
2598
  "it ",
2401
2599
  e.toolCalls,
2402
2600
  "tc \xB7 ",
2403
- fmtElapsed4(taskElapsed)
2601
+ fmtElapsed6(taskElapsed)
2404
2602
  ] }),
2405
2603
  recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
2406
2604
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
@@ -3452,6 +3650,34 @@ function reducer(state, action) {
3452
3650
  case "autoPhaseReset": {
3453
3651
  return { ...state, autoPhase: null };
3454
3652
  }
3653
+ case "worktreeUpsert": {
3654
+ const prev = state.worktrees[action.handleId];
3655
+ const merged = {
3656
+ branch: "",
3657
+ ownerLabel: "",
3658
+ status: "active",
3659
+ insertions: 0,
3660
+ deletions: 0,
3661
+ files: 0,
3662
+ allocatedAt: Date.now(),
3663
+ ...prev,
3664
+ ...action.row
3665
+ };
3666
+ return {
3667
+ ...state,
3668
+ worktrees: { ...state.worktrees, [action.handleId]: merged },
3669
+ worktreeBase: action.baseBranch ?? state.worktreeBase
3670
+ };
3671
+ }
3672
+ case "worktreeRemove": {
3673
+ if (!state.worktrees[action.handleId]) return state;
3674
+ const next = { ...state.worktrees };
3675
+ delete next[action.handleId];
3676
+ return { ...state, worktrees: next };
3677
+ }
3678
+ case "worktreeMonitorToggle": {
3679
+ return { ...state, worktreeMonitorOpen: !state.worktreeMonitorOpen };
3680
+ }
3455
3681
  }
3456
3682
  }
3457
3683
  var PASTE_THRESHOLD_CHARS = 200;
@@ -3625,7 +3851,9 @@ function App({
3625
3851
  rewindOverlay: null,
3626
3852
  eternalStage: null,
3627
3853
  goalSummary: null,
3628
- autoPhase: null
3854
+ autoPhase: null,
3855
+ worktrees: {},
3856
+ worktreeMonitorOpen: false
3629
3857
  });
3630
3858
  const builderRef = useRef(null);
3631
3859
  if (builderRef.current === null) {
@@ -4490,6 +4718,45 @@ function App({
4490
4718
  dispatch({ type: "autoPhaseReset" });
4491
4719
  break;
4492
4720
  }
4721
+ case "worktree.allocated": {
4722
+ const p = payload;
4723
+ dispatch({
4724
+ type: "worktreeUpsert",
4725
+ handleId: p.handleId,
4726
+ baseBranch: p.baseBranch,
4727
+ row: { branch: p.branch, ownerLabel: p.ownerLabel, baseBranch: p.baseBranch, status: "active", allocatedAt: Date.now() }
4728
+ });
4729
+ break;
4730
+ }
4731
+ case "worktree.committed": {
4732
+ const p = payload;
4733
+ dispatch({
4734
+ type: "worktreeUpsert",
4735
+ handleId: p.handleId,
4736
+ row: { insertions: p.insertions, deletions: p.deletions, files: p.files, status: "committing" }
4737
+ });
4738
+ break;
4739
+ }
4740
+ case "worktree.merged": {
4741
+ const p = payload;
4742
+ dispatch({ type: "worktreeUpsert", handleId: p.handleId, row: { status: "merged" } });
4743
+ break;
4744
+ }
4745
+ case "worktree.conflict": {
4746
+ const p = payload;
4747
+ dispatch({ type: "worktreeUpsert", handleId: p.handleId, row: { status: "needs-review", conflictFiles: p.conflictFiles } });
4748
+ break;
4749
+ }
4750
+ case "worktree.failed": {
4751
+ const p = payload;
4752
+ dispatch({ type: "worktreeUpsert", handleId: p.handleId, row: { status: "failed" } });
4753
+ break;
4754
+ }
4755
+ case "worktree.released": {
4756
+ const p = payload;
4757
+ if (!p.kept) dispatch({ type: "worktreeRemove", handleId: p.handleId });
4758
+ break;
4759
+ }
4493
4760
  }
4494
4761
  };
4495
4762
  return subscribeAutoPhase(handler);
@@ -5023,6 +5290,10 @@ function App({
5023
5290
  dispatch({ type: "autoPhaseMonitorToggle" });
5024
5291
  return;
5025
5292
  }
5293
+ if (key.ctrl && input === "t") {
5294
+ dispatch({ type: "worktreeMonitorToggle" });
5295
+ return;
5296
+ }
5026
5297
  if (state.autonomyPicker.open) {
5027
5298
  if (key.escape) {
5028
5299
  dispatch({ type: "autonomyPickerClose" });
@@ -5752,6 +6023,14 @@ User message:
5752
6023
  nowTick,
5753
6024
  onClose: () => dispatch({ type: "autoPhaseMonitorToggle" })
5754
6025
  }
6026
+ ) : state.worktreeMonitorOpen ? /* @__PURE__ */ jsx(
6027
+ WorktreeMonitor,
6028
+ {
6029
+ worktrees: state.worktrees,
6030
+ baseBranch: state.worktreeBase,
6031
+ nowTick,
6032
+ onClose: () => dispatch({ type: "worktreeMonitorToggle" })
6033
+ }
5755
6034
  ) : state.monitorOpen ? /* @__PURE__ */ jsx(
5756
6035
  FleetMonitor,
5757
6036
  {
@@ -5768,7 +6047,8 @@ User message:
5768
6047
  runningPhaseIds: state.autoPhase.runningPhaseIds,
5769
6048
  nowTick
5770
6049
  }
5771
- ) : null
6050
+ ) : null,
6051
+ Object.keys(state.worktrees).length > 0 && !state.worktreeMonitorOpen && !state.monitorOpen ? /* @__PURE__ */ jsx(WorktreePanel, { worktrees: state.worktrees, nowTick }) : null
5772
6052
  ] });
5773
6053
  }
5774
6054
  function renderRunningTools(running) {
@@ -5803,6 +6083,75 @@ function fmtTok3(n) {
5803
6083
  return `${(n / 1e6).toFixed(1)}M`;
5804
6084
  }
5805
6085
 
6086
+ // src/terminal-title.ts
6087
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6088
+ var setTitle = (s2) => `\x1B]0;${s2}\x07`;
6089
+ function shortModel(model) {
6090
+ if (!model) return "";
6091
+ const base = model.split("/").pop() ?? model;
6092
+ return base.replace(/-\d{8}$/, "").replace(/\[.*?\]$/, "");
6093
+ }
6094
+ function marquee(text, offset, width) {
6095
+ const padded = `${text} `;
6096
+ const start = offset % padded.length;
6097
+ return (padded + padded).slice(start, start + width);
6098
+ }
6099
+ function startTerminalTitle(opts) {
6100
+ const { stdout, events } = opts;
6101
+ if (process.env["WRONGSTACK_NO_TITLE"] === "1" || !stdout.isTTY) {
6102
+ return () => {
6103
+ };
6104
+ }
6105
+ const app = opts.appName ?? "WrongStack";
6106
+ const model = shortModel(opts.model);
6107
+ const idleAfter = opts.idleAfterMs ?? 3500;
6108
+ const suffix = model ? ` \xB7 ${model}` : "";
6109
+ let frame = 0;
6110
+ let scroll = 0;
6111
+ let phase = "idle";
6112
+ let toolName = "";
6113
+ let lastActivity = 0;
6114
+ const touch = (next, tool) => {
6115
+ phase = next;
6116
+ if (tool) toolName = tool;
6117
+ lastActivity = Date.now();
6118
+ };
6119
+ const offs = [
6120
+ events.on("iteration.started", () => touch("thinking")),
6121
+ events.on("provider.text_delta", () => touch("thinking")),
6122
+ events.on("provider.thinking_delta", () => touch("thinking")),
6123
+ events.on("tool.started", (e) => touch("tool", e.name ?? "tool")),
6124
+ events.on("tool.executed", () => touch("thinking"))
6125
+ ];
6126
+ const write = (s2) => {
6127
+ try {
6128
+ stdout.write(s2);
6129
+ } catch {
6130
+ }
6131
+ };
6132
+ const timer = setInterval(() => {
6133
+ frame = (frame + 1) % SPINNER.length;
6134
+ scroll += 1;
6135
+ if (lastActivity && Date.now() - lastActivity > idleAfter) phase = "idle";
6136
+ const sp = SPINNER[frame];
6137
+ let title;
6138
+ if (phase === "tool") {
6139
+ title = `${sp} \u25B8 ${toolName}${suffix}`;
6140
+ } else if (phase === "thinking") {
6141
+ title = `${sp} thinking\u2026${suffix}`;
6142
+ } else {
6143
+ title = marquee(`\u2726 ${app}${suffix}`, scroll >> 1, 22);
6144
+ }
6145
+ write(setTitle(title));
6146
+ }, opts.intervalMs ?? 130);
6147
+ timer.unref?.();
6148
+ return () => {
6149
+ clearInterval(timer);
6150
+ for (const off of offs) off();
6151
+ write(setTitle(model ? `${app} \xB7 ${model}` : app));
6152
+ };
6153
+ }
6154
+
5806
6155
  // src/run-tui.ts
5807
6156
  var BRACKETED_PASTE_ON = "\x1B[?2004h";
5808
6157
  var BRACKETED_PASTE_OFF = "\x1B[?2004l";
@@ -5824,6 +6173,7 @@ async function runTui(opts) {
5824
6173
  stdout.write(CURSOR_HOME);
5825
6174
  }
5826
6175
  stdout.write(BRACKETED_PASTE_ON);
6176
+ const stopTitle = startTerminalTitle({ stdout, events: opts.events, model: opts.model });
5827
6177
  const swallowSignals = ["SIGTSTP", "SIGQUIT", "SIGTTIN", "SIGTTOU"];
5828
6178
  const swallow = () => {
5829
6179
  };
@@ -5837,6 +6187,10 @@ async function runTui(opts) {
5837
6187
  const cleanup = () => {
5838
6188
  if (cleaned) return;
5839
6189
  cleaned = true;
6190
+ try {
6191
+ stopTitle();
6192
+ } catch {
6193
+ }
5840
6194
  try {
5841
6195
  stdout.write(BRACKETED_PASTE_OFF);
5842
6196
  if (useAltScreen) {