@dadado/agent-kit-cli 6.3.0 → 6.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.
Files changed (48) hide show
  1. package/dashboard/dashboard-data.mjs +14 -2
  2. package/dashboard/dashboard.html +190 -32
  3. package/dashboard/lib/chat-sessions.d.mts +63 -0
  4. package/dashboard/lib/chat-sessions.mjs +430 -0
  5. package/dashboard/{logo-cursor.svg → logo-bare-metal.svg} +1 -1
  6. package/dashboard/logo-marketplace.svg +1 -1
  7. package/dashboard/serve.mjs +5 -1
  8. package/dashboard/social-preview.svg +2 -2
  9. package/dist/{add-VA2W7I6P.js → add-IT6XFPKT.js} +2 -2
  10. package/dist/{chunk-NNKOWUWM.js → chunk-236IYK6L.js} +64 -28
  11. package/dist/{chunk-YRFSTHR5.js → chunk-2OIPMQHA.js} +13 -219
  12. package/dist/{chunk-JEI6HACW.js → chunk-2TCRKQHD.js} +35 -379
  13. package/dist/{chunk-U26MRIWN.js → chunk-34BBPQKI.js} +1 -1
  14. package/dist/{chunk-GUQYNUV4.js → chunk-4XZXM2ZA.js} +1 -1
  15. package/dist/{chunk-Z7RYIHHX.js → chunk-5LQP4JGH.js} +3 -3
  16. package/dist/{chunk-6KUSG7BJ.js → chunk-5WSFO35Y.js} +4 -1
  17. package/dist/{chunk-MAPOQEA2.js → chunk-6RRG3WOE.js} +8 -1
  18. package/dist/{chunk-OWWFKGKS.js → chunk-EI3VQ7FU.js} +2 -2
  19. package/dist/{chunk-QTBTKEXK.js → chunk-FM32XR7A.js} +1 -1
  20. package/dist/{chunk-KVY425NM.js → chunk-GIZNM73K.js} +4 -2
  21. package/dist/{chunk-MO63KZBP.js → chunk-HKYJG2FJ.js} +8 -6
  22. package/dist/{chunk-SV2BR2LZ.js → chunk-I2D75F4I.js} +7 -7
  23. package/dist/{chunk-25J2SMJQ.js → chunk-IN4MQ3P3.js} +1 -1
  24. package/dist/{chunk-OVOXRH5W.js → chunk-MEZUW3LL.js} +1 -1
  25. package/dist/chunk-MTLQN57O.js +380 -0
  26. package/dist/chunk-R6WH5P7R.js +223 -0
  27. package/dist/{chunk-VQBI3JVS.js → chunk-TNO7GTAJ.js} +2 -2
  28. package/dist/{chunk-Z7ABX5QI.js → chunk-U7XOLYDG.js} +4 -2
  29. package/dist/{chunk-WMVMBPCR.js → chunk-WOGBBJCX.js} +1 -1
  30. package/dist/{chunk-6GYKOHIL.js → chunk-YIOTJQHL.js} +2 -2
  31. package/dist/{contribute-NLM2BN6Z.js → contribute-V7PV5UMK.js} +3 -3
  32. package/dist/{countdown-QXJD6TDB.js → countdown-YONBTSJ2.js} +10 -8
  33. package/dist/{diff-HHG4JEDE.js → diff-GWIHGH65.js} +4 -4
  34. package/dist/{doctor-TWF2M3S3.js → doctor-FLIQCVFN.js} +8 -8
  35. package/dist/engine-info-UQNVNOA2.js +89 -0
  36. package/dist/{ground-control-NMYYIVCV.js → ground-control-6W2AKTKI.js} +5 -4
  37. package/dist/{guard-EHXF76ID.js → guard-ZZRQW3DJ.js} +107 -8
  38. package/dist/{hook-VP7FHRSZ.js → hook-2SX2V2T6.js} +10 -4
  39. package/dist/index.js +26 -22
  40. package/dist/{init-UOTV4ASJ.js → init-4B2IRH27.js} +11 -11
  41. package/dist/{install-VCHPXQV3.js → install-3JVTG35O.js} +11 -11
  42. package/dist/{run-R66G276Q.js → run-AYT6CRHI.js} +12 -9
  43. package/dist/{run-plan-24Z5K4P3.js → run-plan-5XIISG3W.js} +11 -7
  44. package/dist/sessions-O7MYEMCT.js +343 -0
  45. package/dist/{setup-global-M6LKGJHQ.js → setup-global-6JM5XTAT.js} +5 -5
  46. package/dist/{status-5UKVIKOI.js → status-PMAI7NH3.js} +6 -6
  47. package/dist/{update-VQBGEUWS.js → update-3PLY4FZA.js} +9 -9
  48. package/package.json +1 -1
@@ -7,11 +7,13 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join, resolve } from "node:path";
10
+ import { collectChats } from "./lib/chat-sessions.mjs";
10
11
  import { collectGateExitTickets } from "./lib/gate-exit-tickets.mjs";
11
12
  import {
12
13
  allowlistConfig,
13
14
  MAX_STRING,
14
15
  parseGitStatusShort,
16
+ resolveBroadcastAuth,
15
17
  resolveSnapshotRepoRoot,
16
18
  truncateStr,
17
19
  } from "./lib/guards.mjs";
@@ -133,7 +135,7 @@ function redactTerminalOutput(text) {
133
135
 
134
136
  const SNAPSHOT = {
135
137
  _schema: {
136
- version: "1.6.0",
138
+ version: "1.7.0",
137
139
  description: "Mission Control dashboard data model",
138
140
  fields: {
139
141
  generatedAt: "ISO-8601 timestamp of snapshot generation",
@@ -154,6 +156,8 @@ const SNAPSHOT = {
154
156
  "Newest headless agent-kit run / tick logs from <state root>/loop-logs (AGENT_KIT_STATE_ROOT: .cursor by default, or .agent-kit; no ~/.cursor dependency): format, session id, capped last agent text, result, LOOP_TICK_RESULT line, HITL gate ids and reply stamps (1.5.0)",
155
157
  gateExit:
156
158
  "Pending operator tickets (read-only, never fetches): {marker: absent|ok|invalid, base, tickets[] (stamp, headline, askId, labels, trigger, openedAt, url, branch, branchState unmerged|merged|none, inMarker), skipped[] (latest launcher skip records), truncated} from .cursor/context/gate-exit.json and gate-exit/<stamp> branches; same contract as agent-kit ground-control status (1.6.0)",
159
+ chats:
160
+ "Saved agent chats of this workspace: {withheld, vendors[]} with one envelope per vendor {vendor: claude|codex|cursor, status: ok|unsupported|none, projectDir, sessions[] ({id, title, titleSource: custom|ai|first-prompt|none, modified unix ms, sizeBytes}, newest first, capped at 200), total}; same contract as agent-kit sessions --json. withheld is true (vendors empty) in broadcast mode: chat titles never leave the machine (1.7.0)",
157
161
  processes:
158
162
  "Running process snapshots (node, serve.mjs, git operations) with elapsed time and a generated narration per process",
159
163
  skills: "Available skills discovered in .cursor/skills/",
@@ -162,7 +166,7 @@ const SNAPSHOT = {
162
166
  },
163
167
  },
164
168
  generatedAt: new Date().toISOString(),
165
- dashboardDataVersion: "1.6.0",
169
+ dashboardDataVersion: "1.7.0",
166
170
  plans: [],
167
171
  system: {
168
172
  repoRoot: ROOT,
@@ -175,6 +179,7 @@ const SNAPSHOT = {
175
179
  terminals: [],
176
180
  runLogs: [],
177
181
  gateExit: { marker: "absent", base: "staging", tickets: [], skipped: [], truncated: false },
182
+ chats: { withheld: false, vendors: [] },
178
183
  processes: [],
179
184
  skills: [],
180
185
  health: { status: "ok", checks: [] },
@@ -789,6 +794,13 @@ if (withinSnapshotBudget(200)) {
789
794
  SNAPSHOT.gateExit = collectGateExitTickets(ROOT, { redact: redactTerminalOutput });
790
795
  }
791
796
 
797
+ // 7d. Saved agent chats (~/.claude/projects/<slug>/<uuid>.jsonl), read-only. Titles are private:
798
+ // a broadcast (non-loopback, token-gated) server never gets them (serve.mjs strips them too).
799
+ SNAPSHOT.chats = collectChats({
800
+ cwd: ROOT,
801
+ withhold: resolveBroadcastAuth(process.env).broadcast !== false,
802
+ });
803
+
792
804
  // 8. Config
793
805
  const configPath = join(ROOT, ".cursor", "context", "config.json");
794
806
  if (existsSync(configPath)) {
@@ -5,19 +5,25 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>Mission Control</title>
8
- <link rel="icon" type="image/svg+xml" id="favicon" href="/logo.svg">
8
+ <link rel="icon" type="image/svg+xml" id="favicon" href="/logo-bare-metal.svg">
9
9
  <script>
10
- /* Restore dashboard skin before first paint (namespaced local preference). */
10
+ /* Restore dashboard skin before first paint (namespaced local preference).
11
+ Default Bare Metal; a stored 'cursor' (the pre-rename id) is rewritten to
12
+ 'bare-metal'. Keep in sync with normalizeDashboardSkin below. */
11
13
  (function () {
12
14
  var key = 'agent-kit:dashboard-skin';
13
- var skin = 'legacy';
15
+ var skin = 'bare-metal';
14
16
  try {
15
17
  var stored = localStorage.getItem(key);
16
- if (stored === 'cursor' || stored === 'legacy') skin = stored;
18
+ if (stored === 'cursor') {
19
+ localStorage.setItem(key, 'bare-metal');
20
+ } else if (stored === 'bare-metal' || stored === 'legacy') {
21
+ skin = stored;
22
+ }
17
23
  } catch (e) { /* private mode / blocked storage */ }
18
24
  document.documentElement.setAttribute('data-dashboard-skin', skin);
19
25
  var fav = document.getElementById('favicon');
20
- if (fav) fav.setAttribute('href', skin === 'cursor' ? '/logo-cursor.svg' : '/logo.svg');
26
+ if (fav) fav.setAttribute('href', skin === 'legacy' ? '/logo.svg' : '/logo-bare-metal.svg');
21
27
  })();
22
28
  /* Restore Crew Monitor row density before first paint; auto-pick by viewport
23
29
  when the operator has no stored preference. Same namespaced-key + blocked-
@@ -38,18 +44,10 @@
38
44
  <style>
39
45
  /* ===== Reset & Base ===== */
40
46
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
41
- /* Legacy skin (default): current Mission Control palette. */
42
- :root,
43
- html[data-dashboard-skin="legacy"] {
44
- --bg-primary: #0b0e14;
45
- --bg-secondary: #111620;
46
- --bg-card: #161c2a;
47
- --bg-card-hover: #1b2335;
48
- --border: #1e2a3a;
49
- --border-active: #2a3a5a;
50
- --text-primary: #e2e8f0;
51
- --text-secondary: #8899aa;
52
- --text-muted: #6d8094; /* landing uses #7f93a8 (AA over marketing gradient); see docs/design-system.md */
47
+ /* Shared tokens (both Interface Skins): semantic colors and structure.
48
+ A skin overrides only the 9 surface variables below; the token sets are
49
+ published in docs/contracts/mission-control-skins.json. */
50
+ :root {
53
51
  --green: #22c55e;
54
52
  --green-bg: rgba(34,197,94,0.12);
55
53
  --yellow: #eab308;
@@ -112,8 +110,9 @@ html[data-dashboard-skin="legacy"] {
112
110
  /* Shared status / badge circles (#statusDot and .dot base). */
113
111
  --mc-status-dot-size: 7px;
114
112
  }
115
- /* Cursor skin: Cursor-like base chrome only; keep Agent Kit semantic highlights. */
116
- html[data-dashboard-skin="cursor"] {
113
+ /* Bare Metal skin (default): neutral dark surfaces. */
114
+ :root,
115
+ html[data-dashboard-skin="bare-metal"] {
117
116
  --bg-primary: #141414;
118
117
  --bg-secondary: #181818;
119
118
  --bg-card: #1c1c1c;
@@ -122,7 +121,19 @@ html[data-dashboard-skin="cursor"] {
122
121
  --border-active: #3a3a3a;
123
122
  --text-primary: #e4e4e4;
124
123
  --text-secondary: #a0a0a0;
125
- --text-muted: #6e6e6e;
124
+ --text-muted: #8b8b8b; /* >= 4.5:1 (WCAG AA) on every bg-* surface incl. hover; see docs/design-system.md */
125
+ }
126
+ /* Legacy skin: the original blue-slate Mission Control surfaces. */
127
+ html[data-dashboard-skin="legacy"] {
128
+ --bg-primary: #0b0e14;
129
+ --bg-secondary: #111620;
130
+ --bg-card: #161c2a;
131
+ --bg-card-hover: #1b2335;
132
+ --border: #1e2a3a;
133
+ --border-active: #2a3a5a;
134
+ --text-primary: #e2e8f0;
135
+ --text-secondary: #8899aa;
136
+ --text-muted: #7f93a8; /* same as the landing; >= 4.5:1 (WCAG AA) on every bg-* surface incl. hover; see docs/design-system.md */
126
137
  }
127
138
  html, body {
128
139
  height: 100%;
@@ -934,6 +945,31 @@ body.mc-fullscreen .top-tabs-row {
934
945
  }
935
946
  .terminal-item:hover { background: var(--bg-card-hover); }
936
947
 
948
+ /* ===== Chats (saved agent sessions) ===== */
949
+ .chat-list { display: flex; flex-direction: column; gap: 4px; }
950
+ .chat-item {
951
+ display: flex;
952
+ align-items: center;
953
+ gap: 10px;
954
+ padding: 6px 10px;
955
+ border-radius: 6px;
956
+ font-size: 12px;
957
+ color: var(--text-secondary);
958
+ }
959
+ .chat-item:hover { background: var(--bg-card-hover); }
960
+ .chat-title {
961
+ flex: 1;
962
+ min-width: 0;
963
+ overflow: hidden;
964
+ text-overflow: ellipsis;
965
+ white-space: nowrap;
966
+ color: var(--text-primary);
967
+ }
968
+ .chat-title[data-untitled="true"] { color: var(--text-muted); font-style: italic; }
969
+ .chat-age,
970
+ .chat-id { color: var(--text-muted); font-family: var(--mc-font-mono); white-space: nowrap; }
971
+ .chat-note { font-size: 12px; color: var(--text-muted); margin: 8px 10px 0; }
972
+
937
973
  /* ===== Healthcenter ===== */
938
974
  .healthcenter {
939
975
  display: flex;
@@ -1230,7 +1266,7 @@ body.mc-fullscreen .top-tabs-row {
1230
1266
  .git-hygiene-paths {
1231
1267
  font-family: var(--mc-font-mono);
1232
1268
  font-size: 11px;
1233
- color: var(--text-muted);
1269
+ color: var(--text-secondary); /* muted fails AA on the --yellow-bg tint */
1234
1270
  margin-top: 4px;
1235
1271
  word-break: break-all;
1236
1272
  }
@@ -3649,7 +3685,7 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
3649
3685
  <!-- Header: identity + one transport/freshness signal + refresh -->
3650
3686
  <div class="header">
3651
3687
  <button type="button" class="header-brand" id="headerBrand" onclick="return goHome()" aria-label="Home, Overview" title="Home, Overview">
3652
- <img class="logo" id="headerLogo" src="/logo.svg" width="28" height="28" alt="" aria-hidden="true"> Mission Control
3688
+ <img class="logo" id="headerLogo" src="/logo-bare-metal.svg" width="28" height="28" alt="" aria-hidden="true"> Mission Control
3653
3689
  <span class="header-workspace" id="headerWorkspace" hidden></span>
3654
3690
  </button>
3655
3691
  <span class="header-version" id="version" hidden>v—</span>
@@ -3711,6 +3747,10 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
3711
3747
  Terminals
3712
3748
  <span class="top-tab-badge" id="navTerminalsBadge">0</span>
3713
3749
  </a>
3750
+ <a class="top-tab nav-more-item" role="menuitem" href="#" data-section="chats" tabindex="0" onclick="return showSection('chats')" aria-label="Chats section">
3751
+ Chats
3752
+ <span class="top-tab-badge" id="navChatsBadge">0</span>
3753
+ </a>
3714
3754
  <a class="top-tab nav-more-item" role="menuitem" href="#" data-section="processes" tabindex="0" onclick="return showSection('processes')" aria-label="Processes section">
3715
3755
  Processes
3716
3756
  <span class="top-tab-badge" id="navProcessesBadge">0</span>
@@ -3721,11 +3761,11 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
3721
3761
  <div class="nav-more-separator" role="separator"></div>
3722
3762
  <div class="nav-more-group" role="group" aria-labelledby="navSkinsLabel">
3723
3763
  <div class="nav-more-group-label" id="navSkinsLabel" data-icon="skins">Skins</div>
3724
- <button type="button" class="nav-more-item" role="menuitemradio" aria-checked="true" data-dashboard-skin-option="legacy" tabindex="0" onclick="return setDashboardSkin('legacy')">
3725
- Legacy
3764
+ <button type="button" class="nav-more-item" role="menuitemradio" aria-checked="true" data-dashboard-skin-option="bare-metal" tabindex="0" onclick="return setDashboardSkin('bare-metal')">
3765
+ Bare Metal
3726
3766
  </button>
3727
- <button type="button" class="nav-more-item" role="menuitemradio" aria-checked="false" data-dashboard-skin-option="cursor" tabindex="0" onclick="return setDashboardSkin('cursor')">
3728
- Cursor
3767
+ <button type="button" class="nav-more-item" role="menuitemradio" aria-checked="false" data-dashboard-skin-option="legacy" tabindex="0" onclick="return setDashboardSkin('legacy')">
3768
+ Legacy
3729
3769
  </button>
3730
3770
  </div>
3731
3771
  </div>
@@ -3882,6 +3922,7 @@ const SECTION_IDS = [
3882
3922
  'devops',
3883
3923
  'memory',
3884
3924
  'terminals',
3925
+ 'chats',
3885
3926
  'processes',
3886
3927
  'config',
3887
3928
  ];
@@ -4061,17 +4102,29 @@ function goCockpitAnchor(anchorId) {
4061
4102
 
4062
4103
  // ===== Dashboard visual skins (More menu; local preference only) =====
4063
4104
  const DASHBOARD_SKIN_KEY = 'agent-kit:dashboard-skin';
4064
- const DASHBOARD_SKINS = ['legacy', 'cursor'];
4105
+ const DASHBOARD_SKINS = ['bare-metal', 'legacy'];
4106
+ const DASHBOARD_SKIN_DEFAULT = 'bare-metal';
4107
+ /** Pre-rename ids still found in browser storage. */
4108
+ const DASHBOARD_SKIN_ALIASES = { cursor: 'bare-metal' };
4065
4109
 
4066
4110
  function normalizeDashboardSkin(skin) {
4067
- return DASHBOARD_SKINS.includes(skin) ? skin : 'legacy';
4111
+ if (Object.prototype.hasOwnProperty.call(DASHBOARD_SKIN_ALIASES, skin)) {
4112
+ return DASHBOARD_SKIN_ALIASES[skin];
4113
+ }
4114
+ return DASHBOARD_SKINS.includes(skin) ? skin : DASHBOARD_SKIN_DEFAULT;
4068
4115
  }
4069
4116
 
4070
4117
  function getStoredDashboardSkin() {
4071
4118
  try {
4072
- return normalizeDashboardSkin(localStorage.getItem(DASHBOARD_SKIN_KEY));
4119
+ const stored = localStorage.getItem(DASHBOARD_SKIN_KEY);
4120
+ const skin = normalizeDashboardSkin(stored);
4121
+ // Rewrite an aliased value (e.g. 'cursor') so storage holds the current id.
4122
+ if (stored !== null && stored !== skin && Object.prototype.hasOwnProperty.call(DASHBOARD_SKIN_ALIASES, stored)) {
4123
+ localStorage.setItem(DASHBOARD_SKIN_KEY, skin);
4124
+ }
4125
+ return skin;
4073
4126
  } catch (e) {
4074
- return 'legacy';
4127
+ return DASHBOARD_SKIN_DEFAULT;
4075
4128
  }
4076
4129
  }
4077
4130
 
@@ -4084,7 +4137,7 @@ function applyDashboardSkin(skin, opts) {
4084
4137
  el.setAttribute('aria-checked', selected ? 'true' : 'false');
4085
4138
  el.classList.toggle('active', selected);
4086
4139
  });
4087
- const logoSrc = next === 'cursor' ? '/logo-cursor.svg' : '/logo.svg';
4140
+ const logoSrc = next === 'legacy' ? '/logo.svg' : '/logo-bare-metal.svg';
4088
4141
  const headerLogo = document.getElementById('headerLogo');
4089
4142
  if (headerLogo) headerLogo.setAttribute('src', logoSrc);
4090
4143
  const favicon = document.getElementById('favicon');
@@ -6321,6 +6374,98 @@ function showTerminalPopup(content, title) {
6321
6374
  document.body.appendChild(overlay);
6322
6375
  }
6323
6376
 
6377
+ /**
6378
+ * POSIX single-quote for a pasted shell command.
6379
+ * Mirror of `shellQuote` in dashboard/lib/chat-sessions.mjs (inline HTML cannot import it).
6380
+ */
6381
+ function chatShellQuote(value) {
6382
+ const s = String(value ?? '');
6383
+ if (/^[A-Za-z0-9_./-]+$/.test(s)) return s;
6384
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6385
+ }
6386
+
6387
+ /** Mirror of `claudeResumeCommand` in dashboard/lib/chat-sessions.mjs. */
6388
+ function chatResumeCommand(workspace, id) {
6389
+ return `cd ${chatShellQuote(workspace)} && claude --resume ${chatShellQuote(id)}`;
6390
+ }
6391
+
6392
+ /** Claude Code envelope of the snapshot `chats` field (null when absent or withheld). */
6393
+ function claudeChatsEnvelope(d) {
6394
+ const vendors = Array.isArray(d?.chats?.vendors) ? d.chats.vendors : [];
6395
+ return vendors.find((v) => v && v.vendor === 'claude') || null;
6396
+ }
6397
+
6398
+ function chatsTotal(d) {
6399
+ const claude = claudeChatsEnvelope(d);
6400
+ return claude && claude.status === 'ok' && Number.isFinite(claude.total) ? claude.total : 0;
6401
+ }
6402
+
6403
+ /** Chats section body. Titles are user content: escapeHtml / escapeAttr only. */
6404
+ function renderChatsSection(d) {
6405
+ const chats = d?.chats || null;
6406
+ const workspace = d?.system?.repoRoot || '';
6407
+ const claude = claudeChatsEnvelope(d);
6408
+ const sessions = claude && claude.status === 'ok' && Array.isArray(claude.sessions) ? claude.sessions : [];
6409
+ const total = chatsTotal(d);
6410
+ const unsupported = (Array.isArray(chats?.vendors) ? chats.vendors : [])
6411
+ .filter((v) => v && v.status === 'unsupported')
6412
+ .map((v) => ({ codex: 'Codex', cursor: 'Cursor' }[v.vendor] || String(v.vendor || '')));
6413
+ let subtitle = `${total} saved`;
6414
+ if (sessions.length < total) subtitle = `${sessions.length} newest of ${total}`;
6415
+ let body;
6416
+ if (chats?.withheld === true) {
6417
+ subtitle = 'hidden';
6418
+ body = renderEmptyStateCta({
6419
+ iconKind: 'chats',
6420
+ headline: 'Chats stay private',
6421
+ support: 'This Mission Control is broadcast on the network, so saved chat titles are left out. Open it on this machine to see them.',
6422
+ });
6423
+ } else if (sessions.length === 0) {
6424
+ body = renderEmptyStateCta({
6425
+ iconKind: 'chats',
6426
+ headline: 'No saved chats',
6427
+ support: 'The agent CLI saves each session per folder. Start claude in this workspace folder and it shows up here, ready to resume.',
6428
+ });
6429
+ } else {
6430
+ body = `<div class="chat-list">${sessions.map((s) => {
6431
+ const id = String(s.id || '');
6432
+ const untitled = !s.title;
6433
+ const title = untitled ? 'Untitled chat' : String(s.title);
6434
+ const modifiedIso = Number.isFinite(s.modified) ? new Date(s.modified).toISOString() : '';
6435
+ const cmd = chatResumeCommand(workspace, id);
6436
+ return `
6437
+ <div class="chat-item">
6438
+ <span class="chat-title"${untitled ? ' data-untitled="true"' : ''} title="${escapeAttr(title)}">${escapeHtml(title)}</span>
6439
+ <span class="chat-age" title="${escapeAttr(modifiedIso)}">${escapeHtml(relativeTime(modifiedIso))}</span>
6440
+ <span class="chat-id" title="${escapeAttr(id)}">${escapeHtml(id.slice(0, 8))}</span>
6441
+ <button type="button" class="process-copy-btn chat-resume-btn" data-chat-resume="${escapeAttr(cmd)}" aria-label="${escapeAttr(`Copy the resume command for chat ${title}`)}" title="${escapeAttr(copyActionTitle('resume command', 'terminal'))}">Copy resume</button>
6442
+ </div>`;
6443
+ }).join('')}</div>`;
6444
+ }
6445
+ const note = unsupported.length
6446
+ ? `<p class="chat-note">${escapeHtml(unsupported.map((name) => `${name}: not supported yet`).join(' \u00b7 '))}</p>`
6447
+ : '';
6448
+ return `
6449
+ <div class="content-section" id="section-chats">
6450
+ <div class="section-title">
6451
+ Chats
6452
+ <span class="section-subtitle">${escapeHtml(subtitle)}</span>
6453
+ </div>
6454
+ ${body}
6455
+ ${note}
6456
+ </div>
6457
+ `;
6458
+ }
6459
+
6460
+ function wireChatResumeButtons() {
6461
+ document.querySelectorAll('.chat-resume-btn[data-chat-resume]').forEach((btn) => {
6462
+ btn.addEventListener('click', (e) => {
6463
+ e.stopPropagation();
6464
+ copyForPaste(btn.dataset.chatResume, 'resume command', 'terminal');
6465
+ });
6466
+ });
6467
+ }
6468
+
6324
6469
  function wireTerminalExpandButtons() {
6325
6470
  document.querySelectorAll('.terminal-expand-btn[data-terminal-id]').forEach((btn) => {
6326
6471
  btn.addEventListener('click', (e) => {
@@ -7107,7 +7252,7 @@ function nowMetaIconSvg(kind, opts) {
7107
7252
  * features >= 1 unit. Detail that cannot meet the floor is drawn filled
7108
7253
  * (fill="currentColor" stroke="none"), never as a sub-stroke stroked shape.
7109
7254
  * Static path markup only; never concatenate untrusted text into the SVG.
7110
- * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'processes'|'config'} kind
7255
+ * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'chats'|'processes'|'config'} kind
7111
7256
  * @param {{ decorative?: boolean }} [opts] decorative true (default): aria-hidden next to a visible label.
7112
7257
  * decorative false: role=img + aria-label + title for icon-only controls (e.g. more-sections).
7113
7258
  */
@@ -7132,6 +7277,7 @@ function spaceIconSvg(kind, opts) {
7132
7277
  devops: 'DevOps',
7133
7278
  memory: 'Memory',
7134
7279
  terminals: 'Terminals',
7280
+ chats: 'Chats',
7135
7281
  processes: 'Processes',
7136
7282
  config: 'Config',
7137
7283
  };
@@ -7212,6 +7358,9 @@ function spaceIconSvg(kind, opts) {
7212
7358
  terminals:
7213
7359
  '<path d="M1.5 3.5h13a1 1 0 011 1v7a1 1 0 01-1 1H1.5a1 1 0 01-1-1v-7a1 1 0 011-1z"/>' +
7214
7360
  '<path d="M5.5 12.5v2M10.5 12.5v2M4 14.5h8"/>',
7361
+ // Speech bubble (chats) — Feather message-square on the 16 grid
7362
+ chats:
7363
+ '<path d="M14 10a1.5 1.5 0 01-1.5 1.5H5L2 14.5v-11A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5z"/>',
7215
7364
  // Layers (processes)
7216
7365
  processes:
7217
7366
  '<path d="M8 2.5l6 3-6 3-6-3 6-3z"/>' +
@@ -8158,6 +8307,11 @@ function renderUnsafe() {
8158
8307
  const navTerminalsBadge = document.getElementById('navTerminalsBadge');
8159
8308
  navTerminalsBadge.textContent = d.terminals.length;
8160
8309
  navTerminalsBadge.className = 'top-tab-badge';
8310
+ const navChatsBadge = document.getElementById('navChatsBadge');
8311
+ if (navChatsBadge) {
8312
+ navChatsBadge.textContent = chatsTotal(d);
8313
+ navChatsBadge.className = 'top-tab-badge';
8314
+ }
8161
8315
  const navProcessesBadge = document.getElementById('navProcessesBadge');
8162
8316
  navProcessesBadge.textContent = d.processes.length;
8163
8317
  navProcessesBadge.className = 'top-tab-badge';
@@ -8732,6 +8886,9 @@ function renderUnsafe() {
8732
8886
  </div>
8733
8887
  `);
8734
8888
 
8889
+ // ===== Chats =====
8890
+ parts.push(renderChatsSection(d));
8891
+
8735
8892
  // ===== Processes =====
8736
8893
  const processes = d.processes || [];
8737
8894
  // Detached audit-session visibility (phase3): host-wide agent-kit-audit-* PTYs.
@@ -8894,6 +9051,7 @@ function renderUnsafe() {
8894
9051
  contentEl.innerHTML = parts.join('');
8895
9052
 
8896
9053
  wireTerminalExpandButtons();
9054
+ wireChatResumeButtons();
8897
9055
 
8898
9056
  updateTransportChrome();
8899
9057
 
@@ -0,0 +1,63 @@
1
+ export const CHAT_SESSIONS_CAP: number;
2
+ export const CHAT_TITLE_MAX: number;
3
+ export const CHAT_NAMED_TITLE_MAX: number;
4
+ export const CHAT_FULL_SCAN_MAX_BYTES: number;
5
+ export const CHAT_HEAD_BYTES: number;
6
+ export const CHAT_TAIL_BYTES: number;
7
+ export const CHAT_SCAN_BUDGET_BYTES: number;
8
+ export const CHAT_VENDORS: readonly ["claude", "codex", "cursor"];
9
+
10
+ export type ChatVendor = "claude" | "codex" | "cursor";
11
+ export type ChatTitleSource = "custom" | "ai" | "first-prompt" | "none";
12
+
13
+ export interface ChatSession {
14
+ id: string;
15
+ title: string | null;
16
+ titleSource: ChatTitleSource;
17
+ /** Last write, unix milliseconds. */
18
+ modified: number;
19
+ sizeBytes: number;
20
+ }
21
+
22
+ export interface ChatSessionsEnvelope {
23
+ vendor: ChatVendor;
24
+ status: "ok" | "unsupported" | "none";
25
+ projectDir: string | null;
26
+ sessions: ChatSession[];
27
+ total: number;
28
+ }
29
+
30
+ export interface ChatsSnapshot {
31
+ withheld: boolean;
32
+ vendors: ChatSessionsEnvelope[];
33
+ }
34
+
35
+ export interface ChatTitle {
36
+ title: string | null;
37
+ titleSource: ChatTitleSource;
38
+ }
39
+
40
+ export interface ListChatSessionsOptions {
41
+ cwd: string;
42
+ home?: string | null;
43
+ cap?: number;
44
+ /** Bytes read in full before older transcripts fall back to a small head + tail window. */
45
+ scanBudgetBytes?: number;
46
+ cache?: Map<string, ChatTitle>;
47
+ }
48
+
49
+ export function claudeProjectSlug(cwd: string): string;
50
+ export function claudeProjectDir(cwd: string, opts?: { home?: string | null }): string | null;
51
+ export function cleanChatTitle(value: unknown, max?: number): string;
52
+ export function readChatTitle(
53
+ filePath: string,
54
+ window?: { fullScanMaxBytes?: number; headBytes?: number; tailBytes?: number },
55
+ ): ChatTitle;
56
+ export function listClaudeSessions(opts: ListChatSessionsOptions): ChatSessionsEnvelope;
57
+ export function listChatSessions(opts: ListChatSessionsOptions): ChatSessionsEnvelope[];
58
+ export function collectChats(opts: ListChatSessionsOptions & { withhold?: boolean }): ChatsSnapshot;
59
+ export function withholdChats<T extends Record<string, unknown>>(snapshot: T): T;
60
+ export function withholdChatsFromPayload(payload: string): string;
61
+ export function shellQuote(value: string): string;
62
+ export function claudeResumeCommand(workspace: string, id: string): string;
63
+ export function formatChatAge(modifiedMs: number, nowMs?: number): string;