@standardagents/code 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -572,6 +572,20 @@ function gradAt(t) {
572
572
  Math.round(a[2] + (b[2] - a[2]) * k)
573
573
  ];
574
574
  }
575
+ function cyclePos(t) {
576
+ const p = t - Math.floor(t);
577
+ return p < 0.5 ? p * 2 : 2 - p * 2;
578
+ }
579
+ function colorAtCycle(tri, tc) {
580
+ if (tc) {
581
+ const [r, g, b] = gradAt(tri);
582
+ return `\x1B[38;2;${r};${g};${b}m`;
583
+ }
584
+ return `\x1B[38;5;${GRAD_256[Math.min(GRAD_256.length - 1, Math.floor(tri * GRAD_256.length))]}m`;
585
+ }
586
+ function brandCycleColor(ms, periodMs = 1200) {
587
+ return colorAtCycle(cyclePos(ms / periodMs), truecolor());
588
+ }
575
589
  function gradientText(text, phase = 0) {
576
590
  const chars = [...text];
577
591
  const visible = chars.filter((ch) => ch.trim().length > 0).length;
@@ -1832,6 +1846,130 @@ var SystemEvents = class {
1832
1846
  }
1833
1847
  };
1834
1848
 
1849
+ // src/wordmill.ts
1850
+ var MILL_WORDS = [
1851
+ "Working",
1852
+ "Thinking",
1853
+ "Building",
1854
+ "Brewing",
1855
+ "Crafting",
1856
+ "Scheming",
1857
+ "Tinkering",
1858
+ "Noodling",
1859
+ "Pondering",
1860
+ "Wrangling",
1861
+ "Conjuring",
1862
+ "Percolating",
1863
+ "Assembling",
1864
+ "Cogitating",
1865
+ "Hatching",
1866
+ "Forging",
1867
+ // The silly shelf — irreverent, never profane.
1868
+ "Pickling",
1869
+ "Spooning",
1870
+ "Marinating",
1871
+ "Squishing",
1872
+ "Wiggling",
1873
+ "Frolicking",
1874
+ "Moisturizing",
1875
+ "Bamboozling",
1876
+ "Skedaddling",
1877
+ "Discombobulating",
1878
+ "Waffling",
1879
+ "Yodeling",
1880
+ "Shimmying",
1881
+ "Galumphing",
1882
+ "Snorkeling",
1883
+ "Bedazzling"
1884
+ ];
1885
+ var FLIP_POOL = "abcdefghjkmnopqrstuvwxyz#$%&@!*+=.:~";
1886
+ var HOLD_MS = 2600;
1887
+ var LAZY_MS = 320;
1888
+ var LAZY_PERIOD = 200;
1889
+ var FAST_PERIOD = 70;
1890
+ var BOLD = "\x1B[1m";
1891
+ var DIM2 = "\x1B[2m";
1892
+ var OFF = "\x1B[22m";
1893
+ function glyphAt(slot, bucket) {
1894
+ let h = (slot + 1) * 2654435761 ^ (bucket + 1) * 40503;
1895
+ h = Math.imul(h ^ h >>> 13, 1274126177);
1896
+ h ^= h >>> 16;
1897
+ return FLIP_POOL[(h >>> 0) % FLIP_POOL.length];
1898
+ }
1899
+ var WordMill = class {
1900
+ word = MILL_WORDS[0];
1901
+ target = null;
1902
+ // non-null while morphing
1903
+ phaseAt = 0;
1904
+ // clock time the current phase began
1905
+ slots = [];
1906
+ morphLen = 0;
1907
+ rng;
1908
+ constructor(rng = Math.random) {
1909
+ this.rng = rng;
1910
+ }
1911
+ /** Restart at "Working" (called when a turn begins). */
1912
+ reset(now) {
1913
+ this.word = MILL_WORDS[0];
1914
+ this.target = null;
1915
+ this.phaseAt = now;
1916
+ }
1917
+ /** The plain word currently displayed or being formed (for tests). */
1918
+ get current() {
1919
+ return this.target ?? this.word;
1920
+ }
1921
+ /** The styled label for this frame (contains only bold/dim ANSI). */
1922
+ text(now) {
1923
+ if (this.phaseAt === 0) this.phaseAt = now;
1924
+ if (!this.target) {
1925
+ if (now - this.phaseAt >= HOLD_MS) this.beginMorph(now);
1926
+ else return `${BOLD}${this.word}${OFF}`;
1927
+ }
1928
+ return this.morphFrame(now);
1929
+ }
1930
+ beginMorph(now) {
1931
+ const options = MILL_WORDS.filter((w) => w !== this.word);
1932
+ this.target = options[Math.floor(this.rng() * options.length)];
1933
+ this.phaseAt = now;
1934
+ const n = Math.max(this.word.length, this.target.length);
1935
+ this.slots = [];
1936
+ let maxLand = 0;
1937
+ for (let i = 0; i < n; i++) {
1938
+ const start = this.rng() * 500;
1939
+ const land = 750 + i * 90 + this.rng() * 350;
1940
+ this.slots.push({ start, land });
1941
+ if (land > maxLand) maxLand = land;
1942
+ }
1943
+ this.morphLen = maxLand + 60;
1944
+ }
1945
+ morphFrame(now) {
1946
+ const target = this.target;
1947
+ const t = now - this.phaseAt;
1948
+ if (t >= this.morphLen) {
1949
+ this.word = target;
1950
+ this.target = null;
1951
+ this.phaseAt = now;
1952
+ return `${BOLD}${this.word}${OFF}`;
1953
+ }
1954
+ let out = "";
1955
+ for (let i = 0; i < this.slots.length; i++) {
1956
+ const s = this.slots[i];
1957
+ if (t < s.start) {
1958
+ const ch = this.word[i];
1959
+ if (ch) out += `${BOLD}${ch}${OFF}`;
1960
+ } else if (t < s.land) {
1961
+ const age = t - s.start;
1962
+ const period = age < LAZY_MS ? LAZY_PERIOD : FAST_PERIOD;
1963
+ out += `${DIM2}${glyphAt(i, Math.floor(t / period))}${OFF}`;
1964
+ } else {
1965
+ const ch = target[i];
1966
+ if (ch) out += `${BOLD}${ch}${OFF}`;
1967
+ }
1968
+ }
1969
+ return out;
1970
+ }
1971
+ };
1972
+
1835
1973
  // src/types.ts
1836
1974
  var LEVELS = [1, 2, 3, 4, 5];
1837
1975
  var LEVEL_DETAIL = {
@@ -1950,6 +2088,7 @@ var C = {
1950
2088
  teal: "\x1B[38;5;37m"
1951
2089
  };
1952
2090
  var FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
2091
+ var SPINNER_MS = 70;
1953
2092
  var SUBAGENT_COLORS = [
1954
2093
  "\x1B[35m",
1955
2094
  // magenta
@@ -1986,8 +2125,13 @@ var Tui = class _Tui {
1986
2125
  // caret's row offset from the input region top, last render
1987
2126
  working = false;
1988
2127
  workingStart = 0;
2128
+ // Slot-machine morph for the working label (Working → Brewing → …).
2129
+ wordMill = new WordMill();
1989
2130
  spinnerTimer = null;
1990
2131
  bgCount = 0;
2132
+ // The `[⚙ n bg]` prompt badge can be selected with ← from the start of the
2133
+ // input (rendered inverted); Enter then opens the background-process panel.
2134
+ bgBadgeSelected = false;
1991
2135
  queuedCount = 0;
1992
2136
  subagents = [];
1993
2137
  // active subagents (one line each)
@@ -2069,6 +2213,9 @@ var Tui = class _Tui {
2069
2213
  /** Up on the top row: return true to consume it (e.g. pull a queued message)
2070
2214
  * before history recall gets a chance. */
2071
2215
  onUpArrow = () => false;
2216
+ /** Enter while the `[⚙ n bg]` badge is selected — opens the bg process panel. */
2217
+ onBgBadge = () => {
2218
+ };
2072
2219
  onQuit = () => process.exit(0);
2073
2220
  levelListeners = [];
2074
2221
  get colors() {
@@ -2146,6 +2293,21 @@ var Tui = class _Tui {
2146
2293
  return;
2147
2294
  }
2148
2295
  if (!key) return;
2296
+ if (this.bgBadgeSelected) {
2297
+ if (key.name === "return" || key.name === "enter") {
2298
+ this.bgBadgeSelected = false;
2299
+ this.renderBottom();
2300
+ this.onBgBadge();
2301
+ return;
2302
+ }
2303
+ if (key.name === "left") return;
2304
+ this.bgBadgeSelected = false;
2305
+ if (key.name === "right" || key.name === "escape") {
2306
+ this.renderBottom();
2307
+ return;
2308
+ }
2309
+ this.renderBottom();
2310
+ }
2149
2311
  if (this.paletteOpen()) {
2150
2312
  const matches = this.filteredCommands();
2151
2313
  const cur = matches.length ? Math.min(this.slashIdx, matches.length - 1) : 0;
@@ -2196,6 +2358,9 @@ var Tui = class _Tui {
2196
2358
  if (this.cursorPos > 0) {
2197
2359
  this.cursorPos--;
2198
2360
  this.renderBottom();
2361
+ } else if (this.bgCount > 0) {
2362
+ this.bgBadgeSelected = true;
2363
+ this.renderBottom();
2199
2364
  }
2200
2365
  return;
2201
2366
  }
@@ -2471,7 +2636,8 @@ var Tui = class _Tui {
2471
2636
  return `${(n / 1e6).toFixed(2)}M`;
2472
2637
  }
2473
2638
  spinnerFrame() {
2474
- return `${C.bold}${C.cyan}${FRAMES[Math.floor(Date.now() / 100) % FRAMES.length]}${C.reset}`;
2639
+ const now = Date.now();
2640
+ return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_MS) % FRAMES.length]}${C.reset}`;
2475
2641
  }
2476
2642
  /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
2477
2643
  tokensText() {
@@ -2544,7 +2710,7 @@ var Tui = class _Tui {
2544
2710
  if (this.working) {
2545
2711
  const el = this.formatElapsed(Date.now() - this.workingStart);
2546
2712
  const right = `${C.dim}${el}${C.reset}${tk ? " " + tk : ""}`;
2547
- const head = `${this.spinnerFrame()} ${C.bold}Working${C.reset}`;
2713
+ const head = `${this.spinnerFrame()} ${this.wordMill.text(Date.now())}`;
2548
2714
  const avail = Math.max(
2549
2715
  0,
2550
2716
  cols2 - this.visibleWidth(head) - this.visibleWidth(right) - 2 - gaugeReserve
@@ -2570,7 +2736,8 @@ var Tui = class _Tui {
2570
2736
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
2571
2737
  promptPrefix() {
2572
2738
  const q = this.queuedCount > 0 ? `${C.yellow}[\u23F3 ${this.queuedCount} queued]${C.reset} ` : "";
2573
- const bg = this.bgCount > 0 ? `${C.cyan}[\u2699 ${this.bgCount} bg]${C.reset} ` : "";
2739
+ const bgText = `[\u2699 ${this.bgCount} bg]`;
2740
+ const bg = this.bgCount > 0 ? this.bgBadgeSelected ? `${C.cyan}\x1B[7m${bgText}\x1B[27m${C.reset} ` : `${C.cyan}${bgText}${C.reset} ` : "";
2574
2741
  return `${q}${bg}${this.levelColor()}\u276F${C.reset} `;
2575
2742
  }
2576
2743
  visibleWidth(s) {
@@ -2672,7 +2839,7 @@ var Tui = class _Tui {
2672
2839
  const quitLine = this.quitArmed ? `${C.dim}Press Control-C again to exit${C.reset}` : null;
2673
2840
  const quitRows = quitLine ? 1 : 0;
2674
2841
  if (quitLine) writeHudRow(quitLine);
2675
- const frame = FRAMES[Math.floor(Date.now() / 100) % FRAMES.length];
2842
+ const frame = FRAMES[Math.floor(Date.now() / SPINNER_MS) % FRAMES.length];
2676
2843
  for (const sub of this.subagents) {
2677
2844
  const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
2678
2845
  const budget = cols2 - 10;
@@ -2961,6 +3128,7 @@ var Tui = class _Tui {
2961
3128
  if (on && !this.working) {
2962
3129
  this.working = true;
2963
3130
  this.workingStart = Date.now();
3131
+ this.wordMill.reset(this.workingStart);
2964
3132
  this.goalComplete = false;
2965
3133
  } else if (!on) {
2966
3134
  this.working = false;
@@ -2992,7 +3160,7 @@ var Tui = class _Tui {
2992
3160
  syncSpinner() {
2993
3161
  const spinning = this.working || this.subagents.length > 0;
2994
3162
  if (spinning && !this.spinnerTimer) {
2995
- this.spinnerTimer = setInterval(() => this.renderBottom(), 100);
3163
+ this.spinnerTimer = setInterval(() => this.renderBottom(), SPINNER_MS);
2996
3164
  } else if (!spinning && this.spinnerTimer) {
2997
3165
  clearInterval(this.spinnerTimer);
2998
3166
  this.spinnerTimer = null;
@@ -3003,6 +3171,7 @@ var Tui = class _Tui {
3003
3171
  }
3004
3172
  setBackgroundCount(n) {
3005
3173
  this.bgCount = n;
3174
+ if (n === 0) this.bgBadgeSelected = false;
3006
3175
  this.renderBottom();
3007
3176
  }
3008
3177
  setQueuedCount(n) {
@@ -3047,6 +3216,7 @@ var Tui = class _Tui {
3047
3216
  }
3048
3217
  // ─── takeover helpers (approval / menus) ───────────────────────────────────
3049
3218
  beginTakeover() {
3219
+ this.bgBadgeSelected = false;
3050
3220
  this.clearBottom();
3051
3221
  process.stdout.write("\r\x1B[K");
3052
3222
  process.stdout.write("\x1B[?25l");
@@ -3269,8 +3439,8 @@ function appendHistory(store, threadId, history, text) {
3269
3439
  // src/markdown.ts
3270
3440
  var ESC = "\x1B[";
3271
3441
  var R = ESC + "0m";
3272
- var BOLD = ESC + "1m";
3273
- var DIM2 = ESC + "2m";
3442
+ var BOLD2 = ESC + "1m";
3443
+ var DIM3 = ESC + "2m";
3274
3444
  var ITAL = ESC + "3m";
3275
3445
  var UNDER = ESC + "4m";
3276
3446
  var TEAL = ESC + "38;5;37m";
@@ -3292,11 +3462,11 @@ function inline(s) {
3292
3462
  });
3293
3463
  s = s.replace(
3294
3464
  /\[([^\]]+)\]\(([^)\s]+)\)/g,
3295
- (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM2}${url}${R}`
3465
+ (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM3}${url}${R}`
3296
3466
  );
3297
- s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD}${t}${R}`);
3467
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD2}${t}${R}`);
3298
3468
  s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
3299
- s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM2}${t}${R}`);
3469
+ s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM3}${t}${R}`);
3300
3470
  s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
3301
3471
  return s;
3302
3472
  }
@@ -3349,7 +3519,7 @@ function renderTable(rows) {
3349
3519
  const cells = [];
3350
3520
  for (let c2 = 0; c2 < cols2; c2++) {
3351
3521
  const raw = r[c2] ?? "";
3352
- const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
3522
+ const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
3353
3523
  cells.push(padEndVisible(styled, widths[c2]));
3354
3524
  }
3355
3525
  out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
@@ -3389,7 +3559,7 @@ function renderMarkdown(src, cols2 = 80) {
3389
3559
  }
3390
3560
  const heading = line.match(/^(#{1,6})\s+(.*)$/);
3391
3561
  if (heading) {
3392
- for (const ln of wrapStyled(heading[2].trim(), cols2)) out.push(`${BOLD}${TEAL}${ln}${R}`);
3562
+ for (const ln of wrapStyled(heading[2].trim(), cols2)) out.push(`${BOLD2}${TEAL}${ln}${R}`);
3393
3563
  i++;
3394
3564
  continue;
3395
3565
  }
@@ -3401,7 +3571,7 @@ function renderMarkdown(src, cols2 = 80) {
3401
3571
  const quote = line.match(/^\s*>\s?(.*)$/);
3402
3572
  if (quote) {
3403
3573
  for (const ln of wrapStyled(inline(quote[1]), Math.max(8, cols2 - 2))) {
3404
- out.push(`${GRAY}\u2502${R} ${DIM2}${ln}${R}`);
3574
+ out.push(`${GRAY}\u2502${R} ${DIM3}${ln}${R}`);
3405
3575
  }
3406
3576
  i++;
3407
3577
  continue;
@@ -3417,7 +3587,7 @@ function renderMarkdown(src, cols2 = 80) {
3417
3587
  if (numbered) {
3418
3588
  const marker = `${numbered[2]}${numbered[3]}`;
3419
3589
  const leadWidth = numbered[1].length + marker.length + 1;
3420
- wrapBlock(out, cols2, `${numbered[1]}${BOLD}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
3590
+ wrapBlock(out, cols2, `${numbered[1]}${BOLD2}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
3421
3591
  i++;
3422
3592
  continue;
3423
3593
  }
@@ -3706,7 +3876,7 @@ var McpManager = class {
3706
3876
  }));
3707
3877
  return {
3708
3878
  ok: true,
3709
- result: servers.length ? JSON.stringify({ servers }, null, 2) : "No MCP servers are connected. The user can add one with the /mcp command, or you can install one with install_mcp."
3879
+ result: servers.length ? JSON.stringify({ servers }, null, 2) : 'No MCP servers are connected. The user can add one with the /mcp command, or you can install one with the mcp tool (action "install").'
3710
3880
  };
3711
3881
  }
3712
3882
  if (action === "list_tools") {
@@ -3839,6 +4009,16 @@ function saveCredential(cred, options = {}) {
3839
4009
  } catch {
3840
4010
  }
3841
4011
  }
4012
+ function deleteCredential(endpoint) {
4013
+ const creds = loadCredentials();
4014
+ delete creds.instances[normalizeEndpoint(endpoint)];
4015
+ fs4.mkdirSync(DIR, { recursive: true });
4016
+ fs4.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
4017
+ try {
4018
+ fs4.chmodSync(FILE, 384);
4019
+ } catch {
4020
+ }
4021
+ }
3842
4022
  function defaultEndpoint() {
3843
4023
  return loadCredentials().default_endpoint ?? null;
3844
4024
  }
@@ -3955,7 +4135,25 @@ function printAssistant(tui, text) {
3955
4135
  }
3956
4136
  tui.print("");
3957
4137
  }
4138
+ function startLoader(label) {
4139
+ const frames = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
4140
+ stdout.write("\x1B[?25l");
4141
+ const draw = () => {
4142
+ const now = Date.now();
4143
+ const f = frames[Math.floor(now / 70) % frames.length];
4144
+ stdout.write(`\r\x1B[K${brandCycleColor(now)}${f}${c.reset} ${c.dim}${label}\u2026${c.reset}`);
4145
+ };
4146
+ draw();
4147
+ const timer = setInterval(draw, 70);
4148
+ return {
4149
+ stop: () => {
4150
+ clearInterval(timer);
4151
+ stdout.write("\r\x1B[K\x1B[?25h");
4152
+ }
4153
+ };
4154
+ }
3958
4155
  function farewell(stoppedProcs = 0) {
4156
+ stdout.write("\x1B[?25h");
3959
4157
  if (stoppedProcs > 0) {
3960
4158
  stdout.write(
3961
4159
  `
@@ -4004,7 +4202,7 @@ function printWelcome(endpoint, projectDir) {
4004
4202
  const meta = [
4005
4203
  `${c.bold}${gradientText("Standard Code")}${c.reset}${version ? ` ${c.dim}v${version}${c.reset}` : ""}`,
4006
4204
  `${c.dim}terminal coding agent${c.reset}`,
4007
- `${c.teal}${host}${c.reset}`,
4205
+ ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c.teal}${host}${c.reset}`],
4008
4206
  `${c.dim}${dir}${c.reset}`
4009
4207
  ];
4010
4208
  const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
@@ -4120,7 +4318,12 @@ ${c.dim}Press Control-C again to exit${c.reset}
4120
4318
  }
4121
4319
  const stored = getCredential(endpoint);
4122
4320
  let api = stored ? new ApiClient(endpoint, stored.access_token) : null;
4123
- const storedCheck = api ? await api.verifyDetailed() : null;
4321
+ let storedCheck = null;
4322
+ if (api) {
4323
+ const connecting = startLoader("Connecting to Standard Agents");
4324
+ storedCheck = await api.verifyDetailed();
4325
+ connecting.stop();
4326
+ }
4124
4327
  if (!api || !storedCheck?.ok) {
4125
4328
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
4126
4329
  if (storedCheck && !storedCheck.ok) {
@@ -4162,7 +4365,9 @@ ${c.dim}Press Control-C again to exit${c.reset}
4162
4365
  });
4163
4366
  if (!got) continue;
4164
4367
  api = new ApiClient(endpoint, got);
4368
+ const connecting2 = startLoader("Connecting to Standard Agents");
4165
4369
  const check2 = await api.verifyDetailed();
4370
+ connecting2.stop();
4166
4371
  if (check2.ok) {
4167
4372
  saveCredential(
4168
4373
  { endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
@@ -4176,7 +4381,9 @@ ${c.dim}Press Control-C again to exit${c.reset}
4176
4381
  continue;
4177
4382
  }
4178
4383
  api = new ApiClient(endpoint, token);
4384
+ const connecting = startLoader("Connecting to Standard Agents");
4179
4385
  const check = await api.verifyDetailed();
4386
+ connecting.stop();
4180
4387
  if (check.ok) {
4181
4388
  saveCredential(
4182
4389
  { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
@@ -4195,18 +4402,20 @@ ${c.dim}Press Control-C again to exit${c.reset}
4195
4402
  handoffClosing = true;
4196
4403
  reader.rl?.close();
4197
4404
  const tags = [`path:${projectDir}`, `machine:${machine}`];
4405
+ const loadingSessions = startLoader("Loading sessions");
4198
4406
  let existing = [];
4199
4407
  try {
4200
4408
  existing = await api.listThreads(AGENT_ID_VARIANTS, tags);
4201
4409
  } catch {
4202
4410
  existing = [];
4203
4411
  }
4412
+ const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
4413
+ loadingSessions.stop();
4204
4414
  const tui = new Tui(1);
4205
4415
  let threadId;
4206
4416
  let resumed = false;
4207
4417
  let historySeed;
4208
4418
  if (existing.length > 0) {
4209
- const summaries = await summarizeThreads(api, existing.slice(0, 8));
4210
4419
  const items = summaries.map((s) => ({
4211
4420
  label: s.label,
4212
4421
  hint: s.hint,
@@ -4230,7 +4439,12 @@ ${c.dim}Press Control-C again to exit${c.reset}
4230
4439
  } else {
4231
4440
  threadId = await api.createThread(AGENT_ID, tags);
4232
4441
  }
4233
- await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
4442
+ for (; ; ) {
4443
+ await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
4444
+ historySeed = threadId;
4445
+ threadId = await api.createThread(AGENT_ID, tags);
4446
+ resumed = false;
4447
+ }
4234
4448
  }
4235
4449
  async function summarizeThreads(api, threads) {
4236
4450
  return Promise.all(
@@ -4367,6 +4581,7 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
4367
4581
  saveApprovals(api, threadId, perm);
4368
4582
  void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
4369
4583
  });
4584
+ const attaching = startLoader("Attaching to thread");
4370
4585
  let busy = false;
4371
4586
  let interrupting = false;
4372
4587
  const queued = [];
@@ -4502,27 +4717,30 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4502
4717
  if (activeSubagents.size > 0) scheduleReconcile();
4503
4718
  }, 5e3);
4504
4719
  heartbeatPoll.unref();
4720
+ let endSession;
4721
+ const sessionEnded = new Promise((r) => endSession = r);
4505
4722
  const quit = async () => {
4506
4723
  tui.end();
4507
- const stopped = api.stop(threadId).catch(() => {
4724
+ const stopped2 = api.stop(threadId).catch(() => {
4508
4725
  });
4509
- const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
4726
+ const procsStopped2 = host.stopAllLocalProcesses().catch(() => 0);
4510
4727
  bridge.close();
4511
4728
  stream.close();
4512
4729
  events.close();
4513
4730
  mcp.closeAll();
4514
- const [, killed] = await Promise.race([
4515
- Promise.all([stopped, procsStopped]),
4731
+ const [, killed2] = await Promise.race([
4732
+ Promise.all([stopped2, procsStopped2]),
4516
4733
  new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
4517
4734
  ]);
4518
- farewell(killed);
4735
+ farewell(killed2);
4519
4736
  process.exit(0);
4520
4737
  };
4521
4738
  tui.setQuitHandler(quit);
4522
- const viewThread = () => {
4523
- const url = `${api.origin}/threads/${threadId}`;
4524
- openUrl(url);
4525
- tui.print(`${c.gray}opened ${c.cyan}${url}${c.reset}`);
4739
+ const logout = async () => {
4740
+ deleteCredential(api.origin);
4741
+ const instanceHost = api.origin.replace(/^https?:\/\//, "");
4742
+ tui.print(`${c.gray}Signed out \u2014 removed the saved token for ${c.teal}${instanceHost}${c.reset}${c.gray}. Run standardcode to sign in again.${c.reset}`);
4743
+ await quit();
4526
4744
  };
4527
4745
  const bgMgr = {
4528
4746
  list: () => registry.list(),
@@ -4554,7 +4772,8 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4554
4772
  return mcpCtl.connect(cfg);
4555
4773
  },
4556
4774
  // Seed the request into the main chat — the agent researches + installs it
4557
- // there (using research_agent + install_mcp), visible in the transcript.
4775
+ // there (using research_agent + the mcp tool's install action), visible in
4776
+ // the transcript.
4558
4777
  requestInstall: (query) => {
4559
4778
  void sendNow(
4560
4779
  `Install an MCP server for me: ${query}. Research the best one and its exact launch command, then install it.`
@@ -4604,14 +4823,21 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4604
4823
  setEnabled: (name, enabled) => api.setSkillEnabled(name, enabled),
4605
4824
  remove: (name) => api.removeSkill(name),
4606
4825
  // Seed the request into the main chat — the agent researches or authors
4607
- // the skill there (research_agent + install_skill), visible in the transcript.
4826
+ // the skill there (research_agent + the skill tool's install action),
4827
+ // visible in the transcript.
4608
4828
  requestInstall: (query) => {
4609
4829
  void sendNow(
4610
- `Install a skill for me: ${query}. Find the skill's published files (or author a proper SKILL.md from your research), install it with install_skill, then tell me what it can do.`
4830
+ `Install a skill for me: ${query}. Find the skill's published files (or author a proper SKILL.md from your research), install it, then tell me what it can do.`
4611
4831
  );
4612
4832
  }
4613
4833
  };
4614
4834
  tui.setCommands([
4835
+ {
4836
+ name: "clear",
4837
+ label: "Clear conversation",
4838
+ hint: "start a fresh session",
4839
+ run: () => endSession()
4840
+ },
4615
4841
  {
4616
4842
  name: "compact",
4617
4843
  label: "Compact conversation now",
@@ -4644,8 +4870,8 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4644
4870
  run: () => runSkillsMenu(tui, skillsCtl)
4645
4871
  },
4646
4872
  { name: "background", label: "Background processes", hint: "list / stop", run: () => runProcessMenu(tui, bgMgr) },
4647
- { name: "view", label: "View thread in AgentBuilder", run: () => viewThread() },
4648
4873
  { name: "keybindings", label: "Keyboard shortcuts", run: () => showKeybindings(tui) },
4874
+ { name: "logout", label: "Sign out", hint: "delete the saved token & quit", run: () => logout() },
4649
4875
  { name: "quit", label: "Quit", run: () => quit() }
4650
4876
  ]);
4651
4877
  const history = await loadHistory(api, threadId, historySeedThreadId);
@@ -4684,6 +4910,10 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4684
4910
  });
4685
4911
  }
4686
4912
  };
4913
+ tui.onBgBadge = () => {
4914
+ void runProcessMenu(tui, bgMgr).catch(() => {
4915
+ });
4916
+ };
4687
4917
  tui.onUpArrow = () => {
4688
4918
  if (tui.getInput().trim() || queued.length === 0) return false;
4689
4919
  const q = queued.pop();
@@ -4696,6 +4926,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4696
4926
  await Promise.all([bridge.connect(), stream.connect()]);
4697
4927
  void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
4698
4928
  });
4929
+ attaching.stop();
4699
4930
  tui.banner([
4700
4931
  `${c.bold}${c.magenta}Standard Code${c.reset} ${c.dim}\u2014 coding agent${c.reset}`,
4701
4932
  `${c.gray}project:${c.reset} ${projectDir}`,
@@ -4795,10 +5026,33 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4795
5026
  } catch {
4796
5027
  }
4797
5028
  };
4798
- setInterval(() => void poll().catch(() => {
5029
+ const pollTimer = setInterval(() => void poll().catch(() => {
4799
5030
  }), 1200);
4800
- await new Promise(() => {
5031
+ await sessionEnded;
5032
+ clearInterval(pollTimer);
5033
+ clearInterval(heartbeatPoll);
5034
+ const stopped = api.stop(threadId).catch(() => {
4801
5035
  });
5036
+ const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
5037
+ bridge.close();
5038
+ stream.close();
5039
+ events.close();
5040
+ mcp.closeAll();
5041
+ const [, killed] = await Promise.race([
5042
+ Promise.all([stopped, procsStopped]),
5043
+ new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
5044
+ ]);
5045
+ tui.setWorking(false);
5046
+ tui.setSubagents([]);
5047
+ tui.setGoal(null);
5048
+ tui.setQueuedCount(0);
5049
+ tui.setContextPct(null);
5050
+ tui.setStep(null, 0);
5051
+ tui.setBackgroundCount(0);
5052
+ if (killed > 0) {
5053
+ tui.print(`${c.cyan}\u2699${c.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
5054
+ }
5055
+ tui.print(`${c.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c.reset}`);
4802
5056
  }
4803
5057
  async function runSkillsMenu(tui, skills) {
4804
5058
  let list;
@@ -4870,6 +5124,7 @@ function showKeybindings(tui) {
4870
5124
  tui.print(`${c.gray} /${c.reset} open the command palette (type to filter)`);
4871
5125
  tui.print(`${c.gray} ctrl-v${c.reset} paste an image from the clipboard ([#Image 1])`);
4872
5126
  tui.print(`${c.gray} \u2191 / \u2193${c.reset} cycle past messages (on the input's top line)`);
5127
+ tui.print(`${c.gray} \u2190${c.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
4873
5128
  tui.print(`${c.gray} ctrl-c${c.reset} quit`);
4874
5129
  }
4875
5130
  async function runProcessMenu(tui, bg) {