dsh-coding-sidebar 1.0.1 → 1.0.3

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 (39) hide show
  1. package/README.md +5 -5
  2. package/lib/client-editor.js +416 -355
  3. package/lib/client-mermaid.js +304 -273
  4. package/lib/client-registry.js +555 -421
  5. package/lib/client-terminal.js +216 -208
  6. package/lib/client.js +557 -423
  7. package/lib/index.js +20 -12
  8. package/lib/types/client/locales.d.ts +4 -0
  9. package/lib/types/context-types.d.ts +19 -10
  10. package/package.json +26 -27
  11. package/src/client/SideCardSection.module.css +2 -2
  12. package/src/client/SideChatView.module.css +12 -12
  13. package/src/client/SubagentView.module.css +41 -0
  14. package/src/client/SubagentView.tsx +63 -7
  15. package/src/client/locales-ar.ts +4 -0
  16. package/src/client/locales-de.ts +4 -0
  17. package/src/client/locales-fr.ts +4 -0
  18. package/src/client/locales-hi.ts +4 -0
  19. package/src/client/locales-id.ts +4 -0
  20. package/src/client/locales-it.ts +4 -0
  21. package/src/client/locales-ja.ts +4 -0
  22. package/src/client/locales-ko.ts +4 -0
  23. package/src/client/locales-nl.ts +4 -0
  24. package/src/client/locales-pl.ts +4 -0
  25. package/src/client/locales-pt.ts +4 -0
  26. package/src/client/locales-ru.ts +4 -0
  27. package/src/client/locales-sv.ts +4 -0
  28. package/src/client/locales-th.ts +4 -0
  29. package/src/client/locales-tr.ts +4 -0
  30. package/src/client/locales-vi.ts +4 -0
  31. package/src/client/locales-zh-HK.ts +4 -0
  32. package/src/client/locales-zh-MO.ts +4 -0
  33. package/src/client/locales-zh-TW.ts +4 -0
  34. package/src/client/locales.ts +8 -0
  35. package/src/context-types.ts +12 -7
  36. package/src/index.ts +5 -4
  37. package/src/jobs-routes.ts +3 -2
  38. package/src/sidechat-routes.ts +13 -8
  39. package/src/subagent-live-route.ts +2 -1
package/lib/index.js CHANGED
@@ -2962,7 +2962,7 @@ const MIRROR_MAX_ENTRIES = 200;
2962
2962
  * The live job_output mirror: subscribes to the session append feed and
2963
2963
  * caches the job_output traces the session store's own log can lag behind
2964
2964
  * (after a host restart the store session stays frozen at its rehydration
2965
- * boundary, so `session.events` misses everything appended since — the very
2965
+ * boundary, so a stale snapshot misses everything appended since — the very
2966
2966
  * reads the pane exists to show). Zero DSH writes: the api-proxy pushes the
2967
2967
  * same feed to browsers.
2968
2968
  */
@@ -3025,7 +3025,8 @@ function buildJobsApi(ctx, outputLimit) {
3025
3025
  const sessionId = requireString(payload, "sessionId");
3026
3026
  const id = requireString(payload, "id");
3027
3027
  const bySeq = /* @__PURE__ */ new Map();
3028
- for (const event of ctx.sessions.get(sessionId)?.events ?? []) {
3028
+ const store = ctx.sessions.get(sessionId);
3029
+ for (const event of (store?.snapshotEvents !== void 0 ? store.snapshotEvents() : []) ?? []) {
3029
3030
  const trace = traceOf(event);
3030
3031
  if (trace !== void 0) bySeq.set(trace.seq, trace);
3031
3032
  }
@@ -3420,7 +3421,8 @@ function buildSubagentLiveApi(ctx) {
3420
3421
  if (entry.kind !== "child" || entry.activity !== "running") continue;
3421
3422
  if (entry.label?.startsWith("Side: ") ?? false) continue;
3422
3423
  try {
3423
- const activity = lastActivity(ctx.sessions.get(entry.id)?.events ?? [], 12);
3424
+ const stored = ctx.sessions.get(entry.id);
3425
+ const activity = lastActivity(stored?.snapshotEvents !== void 0 ? stored.snapshotEvents() : [], 12);
3424
3426
  if (activity.text !== void 0 || activity.tool !== void 0) live[entry.id] = activity;
3425
3427
  } catch {}
3426
3428
  }
@@ -3480,8 +3482,10 @@ async function composeChildSetup(ctx, presetId) {
3480
3482
  async function composePersistedSetup(ctx, childId) {
3481
3483
  const persistence = ctx.get("sessionPersistence");
3482
3484
  if (persistence === void 0) return () => Promise.resolve();
3483
- const inspected = await persistence.inspect(childId);
3484
- const presetId = resolvePresetId(inspected.meta, inspected.events);
3485
+ const handle = await persistence.open(childId, "read");
3486
+ const { events } = await handle.read();
3487
+ const presetId = resolvePresetId(handle.header, events);
3488
+ await handle.close();
3485
3489
  const presets = ctx.get("agentPresets");
3486
3490
  if (presets === void 0 || presetId === void 0) return () => Promise.resolve();
3487
3491
  const resolved = await presets.resolve(presetId);
@@ -3542,8 +3546,8 @@ function buildSidechatApi(ctx) {
3542
3546
  const parent = liveThreadAgent(ctx, sessionId);
3543
3547
  if (parent === void 0) throw new SidebarError("sidechat-error", `parent session "${sessionId}" is not running`, 409);
3544
3548
  const parentSession = parent.session;
3545
- const inheritance = buildSidechatInheritance(parentSession.events);
3546
- const { agentPreset, setup } = await composeChildSetup(ctx, resolvePresetId(parentSession.header, parentSession.events));
3549
+ const inheritance = buildSidechatInheritance(parentSession.snapshotEvents());
3550
+ const { agentPreset, setup } = await composeChildSetup(ctx, resolvePresetId(parentSession.header, parentSession.snapshotEvents()));
3547
3551
  const childId = `session-${randomUUID()}`;
3548
3552
  const label = question === "" ? SIDE_NEW_THREAD_TITLE : sideLabel(question);
3549
3553
  const descriptor = snapshotSubagentDescriptor({
@@ -3565,7 +3569,7 @@ function buildSidechatApi(ctx) {
3565
3569
  meta: {
3566
3570
  ...parentSession.header.cwd === void 0 ? {} : { cwd: parentSession.header.cwd },
3567
3571
  parentSession: parentSession.id,
3568
- seedLength: seed.length,
3572
+ isSeeded: seed.length > 0,
3569
3573
  origin: "subagent",
3570
3574
  delegationDepth: (parentSession.header.delegationDepth ?? 0) + 1,
3571
3575
  ...agentPreset === void 0 ? {} : { agentPreset }
@@ -3622,7 +3626,7 @@ function buildSidechatApi(ctx) {
3622
3626
  throw new SidebarError("sidechat-error", `thread resume failed: ${error instanceof Error ? error.message : String(error)}`, 500);
3623
3627
  }
3624
3628
  }
3625
- if (boundaryDelivered(agent.session.events)) admitFollowup(agent, textPrompt(text));
3629
+ if (boundaryDelivered(agent.session.snapshotEvents())) admitFollowup(agent, textPrompt(text));
3626
3630
  else {
3627
3631
  const parts = [SIDE_BOUNDARY_PROMPT];
3628
3632
  const snapshot = pendingSnapshots.get(childId);
@@ -3668,8 +3672,10 @@ function buildSidechatApi(ctx) {
3668
3672
  }
3669
3673
  const persistence = ctx.get("sessionPersistence");
3670
3674
  if (persistence !== void 0) try {
3671
- const inspected = await persistence.inspect(childId);
3672
- const preset = resolvePresetId(inspected.meta, inspected.events);
3675
+ const handle = await persistence.open(childId, "read");
3676
+ const { events } = await handle.read();
3677
+ const preset = resolvePresetId(handle.header, events);
3678
+ await handle.close();
3673
3679
  return {
3674
3680
  live: false,
3675
3681
  ...preset === void 0 ? {} : { preset }
@@ -3749,7 +3755,9 @@ async function sessionCwdOf(ctx, sessionId, clientCwd) {
3749
3755
  }
3750
3756
  const persistence = ctx.get("sessionPersistence");
3751
3757
  if (persistence !== void 0) {
3752
- const metaCwd = (await persistence.inspect(sessionId)).meta.cwd;
3758
+ const handle = await persistence.open(sessionId, "read");
3759
+ const metaCwd = handle.header.cwd;
3760
+ await handle.close();
3753
3761
  if (metaCwd !== void 0 && metaCwd !== "") try {
3754
3762
  return requireAbsolute(metaCwd);
3755
3763
  } catch {
@@ -302,6 +302,8 @@ export declare const zh: {
302
302
  subagentDiagUnsupported: string;
303
303
  subagentDiagUnavailable: string;
304
304
  subagentThinking: string;
305
+ subagentShowHistory: string;
306
+ subagentHideHistory: string;
305
307
  sideChat: string;
306
308
  sideChatNew: string;
307
309
  sideChatUntitled: string;
@@ -329,6 +331,8 @@ export declare const zh: {
329
331
  jobs: string;
330
332
  jobsCount: string;
331
333
  jobsCountRunning: string;
334
+ jobsShowHistory: string;
335
+ jobsHideHistory: string;
332
336
  jobStatusRunning: string;
333
337
  jobStatusStopping: string;
334
338
  jobStatusCompleted: string;
@@ -78,11 +78,11 @@ export interface SidebarSessionStore {
78
78
  get(id: string): {
79
79
  header: SidebarSessionHeader;
80
80
  /**
81
- * The live session's append-only event log (immutable snapshot; absent
81
+ * The live session's immutable event snapshot (0.1.5 Session API; absent
82
82
  * on sessions the runtime has not hydrated). Read-only access — the
83
83
  * jobs.output route replays `job_output` tool/result rows from it.
84
84
  */
85
- events?: readonly SidebarSessionEvent[];
85
+ snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SidebarSessionEvent[];
86
86
  } | undefined;
87
87
  }
88
88
  /**
@@ -276,16 +276,25 @@ export interface SidebarSessionTitleService {
276
276
  };
277
277
  }
278
278
  /** The host session-persistence face (mirror of the sessionPersistence
279
- * service): detached inspection of a persisted session, used to compose the
280
- * recorded preset when a Side Chat thread cold-resumes. */
281
- export interface SidebarSessionPersistenceService {
282
- inspect(sessionId: string): Promise<{
283
- meta: {
284
- cwd?: string;
285
- agentPreset?: string;
286
- };
279
+ * service, 0.1.5 handle-seam form): a short-lived read handle over one
280
+ * persisted session, used to compose the recorded preset when a Side Chat
281
+ * thread cold-resumes. */
282
+ export interface SidebarSessionPersistenceHandle {
283
+ readonly header: {
284
+ cwd?: string;
285
+ agentPreset?: string;
286
+ };
287
+ read(fromSeq?: number, toSeqExclusive?: number, options?: {
288
+ signal?: AbortSignal;
289
+ }): Promise<{
287
290
  events: readonly SidebarSessionEvent[];
288
291
  }>;
292
+ close(): Promise<void>;
293
+ }
294
+ export interface SidebarSessionPersistenceService {
295
+ open(sessionId: string, access: 'read', options?: {
296
+ signal?: AbortSignal;
297
+ }): Promise<SidebarSessionPersistenceHandle>;
289
298
  }
290
299
  /** RPC result slot mirror (`RpcResult<T>` on the wire). */
291
300
  export type SidebarRpcResult<T> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-coding-sidebar",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -85,19 +85,18 @@
85
85
  "license": "MIT",
86
86
  "peerDependencies": {
87
87
  "@deepseek-ai/cordis": "^4.0.1",
88
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
89
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
90
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
91
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
92
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
93
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
94
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
95
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
96
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
97
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
98
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
99
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
100
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
88
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
89
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
90
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
91
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
92
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
93
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
94
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
95
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
96
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
97
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
98
+ "@deepseek-ai/dsh-subagent": "^0.1.5-rc.1",
99
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
101
100
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
102
101
  "react": "^18.2.0",
103
102
  "react-dom": "^18.2.0"
@@ -140,20 +139,20 @@
140
139
  "ws": "^8.18.0"
141
140
  },
142
141
  "devDependencies": {
142
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
143
143
  "@cordisjs/plugin-loader": "^1.0.0-rc.5",
144
- "@deepseek-ai/dsh-agent": "0.1.2-alpha.2",
145
- "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2",
146
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.2",
147
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
148
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2",
149
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2",
150
- "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
151
- "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2",
152
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
153
- "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
154
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
155
- "@deepseek-ai/dsh-subagent": "0.1.2-alpha.2",
156
- "@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
144
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
145
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
146
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
147
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.1",
148
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
149
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
150
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
151
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
152
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
153
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
154
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
155
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
157
156
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
158
157
  "@types/node": "^24.0.0",
159
158
  "@types/react": "~18.3.1",
@@ -76,7 +76,7 @@
76
76
  .count {
77
77
  padding: 1px 8px;
78
78
  border-radius: 999px;
79
- background: var(--dsw-alias-accent-soft, var(--dsw-alias-bg-layer-2));
79
+ background: var(--dsw-alias-bg-layer-2);
80
80
  font-size: 11px;
81
81
  line-height: 16px;
82
82
  font-weight: 500;
@@ -836,7 +836,7 @@
836
836
  .versionBadgeTag {
837
837
  padding: 1px 8px;
838
838
  border-radius: 999px;
839
- background: var(--dsw-alias-accent-soft, var(--dsw-alias-border-l2));
839
+ background: var(--dsw-alias-border-l2);
840
840
  color: var(--dsw-alias-label-secondary);
841
841
  font-variant-numeric: tabular-nums;
842
842
  }
@@ -23,7 +23,7 @@
23
23
  gap: 4px;
24
24
  min-height: 36px;
25
25
  padding: 4px 8px 4px 12px;
26
- border-bottom: 1px solid var(--dsw-alias-hairline);
26
+ border-bottom: 1px solid var(--dsw-alias-border-l);
27
27
  }
28
28
 
29
29
  .sidechatHeaderDot {
@@ -39,7 +39,7 @@
39
39
  flex: none;
40
40
  max-width: 55%;
41
41
  padding: 1px 8px;
42
- border: 1px solid var(--dsw-alias-hairline);
42
+ border: 1px solid var(--dsw-alias-border-l);
43
43
  border-radius: 999px;
44
44
  font: var(--dsw-font-xxs-12);
45
45
  color: var(--dsw-alias-label-tertiary);
@@ -108,9 +108,9 @@
108
108
  padding: 6px 14px;
109
109
  border: none;
110
110
  border-radius: 999px;
111
- background: var(--dsw-alias-button-info-fill, var(--dsw-alias-accent));
112
- color: var(--dsw-alias-button-info-label, var(--dsw-alias-accent-ink, #fff));
113
- font: var(--dsw-font-s-13);
111
+ background: var(--dsw-alias-button-info-fill, var(--dsw-alias-interactive-bg-hover-solid));
112
+ color: #fff;
113
+ font: var(--dsw-font-s-14);
114
114
  cursor: pointer;
115
115
  transition: opacity 100ms ease-out;
116
116
  }
@@ -135,7 +135,7 @@
135
135
  flex: none;
136
136
  padding: 4px 12px;
137
137
  font: var(--dsw-font-xxs-12);
138
- color: var(--dsw-alias-danger);
138
+ color: #d5304d;
139
139
  }
140
140
 
141
141
  /* ── transcript ─────────────────────────────────────────────────────── */
@@ -246,13 +246,13 @@
246
246
 
247
247
  .sidechatRowFailed .sidechatRowLabel,
248
248
  .sidechatRowFailed .sidechatRowMeta {
249
- color: var(--dsw-alias-danger);
249
+ color: #d5304d;
250
250
  }
251
251
 
252
252
  .sidechatRowBody {
253
253
  margin: 2px 0 4px 7px;
254
254
  padding: 2px 0 2px 10px;
255
- border-left: 1px solid var(--dsw-alias-hairline);
255
+ border-left: 1px solid var(--dsw-alias-border-l);
256
256
  }
257
257
 
258
258
  .sidechatRowProse {
@@ -277,7 +277,7 @@
277
277
  }
278
278
 
279
279
  .sidechatRowCode + .sidechatRowCode {
280
- border-top: 1px solid var(--dsw-alias-hairline);
280
+ border-top: 1px solid var(--dsw-alias-border-l);
281
281
  }
282
282
 
283
283
  /* Shimmer sweep text: generating (streaming row labels, creating hero). */
@@ -328,7 +328,7 @@
328
328
  gap: 4px;
329
329
  margin: 0 8px 8px;
330
330
  padding: 8px 8px 6px 14px;
331
- border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-hairline));
331
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l));
332
332
  border-radius: 16px;
333
333
  background: var(--dsw-specific-input-major, var(--dsw-alias-bg-base));
334
334
  box-shadow: var(--dsw-shadow-lv2, none);
@@ -380,8 +380,8 @@
380
380
  padding: 0;
381
381
  border: none;
382
382
  border-radius: 50%;
383
- background: var(--dsw-alias-button-info-fill, var(--dsw-alias-accent));
384
- color: var(--dsw-alias-button-info-label, var(--dsw-alias-accent-ink, #fff));
383
+ background: var(--dsw-alias-button-info-fill, var(--dsw-alias-interactive-bg-hover-solid));
384
+ color: #fff;
385
385
  cursor: pointer;
386
386
  /* send ⇄ stop swap: keyed remount plays this, hover only fades. */
387
387
  animation: sidechatBtnIn 120ms ease-out;
@@ -274,6 +274,47 @@
274
274
  color: var(--dsw-alias-label-primary);
275
275
  }
276
276
 
277
+ /* ── History collapse toggle (shared: topology levels + jobs list) ─────── */
278
+
279
+ /* The "show earlier rows" disclosure between the header and the collapsed
280
+ rows; full-width like a row, but visually quieter (tertiary, small). */
281
+ .historyToggle {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 5px;
285
+ width: 100%;
286
+ box-sizing: border-box;
287
+ min-height: 26px;
288
+ padding: 3px 8px 3px 11px;
289
+ border: none;
290
+ border-radius: 8px;
291
+ background: transparent;
292
+ font: var(--dsw-font-xxxs-strong-11);
293
+ color: var(--dsw-alias-label-tertiary);
294
+ text-align: left;
295
+ cursor: pointer;
296
+ outline: none;
297
+ }
298
+
299
+ .historyToggle:hover {
300
+ background: var(--dsw-alias-interactive-bg-hover);
301
+ color: var(--dsw-alias-label-secondary);
302
+ }
303
+
304
+ .historyToggle:focus-visible {
305
+ background: var(--dsw-alias-interactive-bg-hover);
306
+ color: var(--dsw-alias-label-secondary);
307
+ }
308
+
309
+ .historyToggle svg {
310
+ flex: none;
311
+ }
312
+
313
+ /* The jobs section's toggle sits right under its list. */
314
+ .jobs .historyToggle {
315
+ margin-top: 2px;
316
+ }
317
+
277
318
  /* ── Background-job section (below the topology tree) ────────────────── */
278
319
 
279
320
  .jobs {
@@ -12,6 +12,12 @@
12
12
  * highlighted in place. Every branch is expanded automatically (lazy
13
13
  * catalogs hydrate on demand and consume live membership while visible).
14
14
  *
15
+ * To keep long histories browsable, each catalog level shows only its
16
+ * LATEST {@link SUBAGENT_VISIBLE} children by default — earlier rows fold
17
+ * behind a history toggle — and the jobs section shows its latest
18
+ * {@link JOBS_VISIBLE} rows the same way. Collapsing is view-only: the
19
+ * header counts, the output dock and live observation still see every row.
20
+ *
15
21
  * Each node card carries live status (state dot, durable label, mode and
16
22
  * activity); while a child RUNS, its card additionally shows the LAST text
17
23
  * output and LAST tool call pulled from its history tail, auto-refreshing
@@ -23,6 +29,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent }
23
29
  import { useSyncExternalStore } from 'react'
24
30
  import clsx from 'clsx'
25
31
  import {
32
+ IconChevronDownOutline14, IconChevronRightOutline14,
26
33
  IconRefreshOutline14, StateDot,
27
34
  } from '@deepseek-ai/dsh-client-ui-primitives'
28
35
  import type {
@@ -65,6 +72,15 @@ const ARGS_PREVIEW = 60
65
72
  const JOB_POLL_MS = 2000
66
73
  /** How long the kill button stays armed before it needs re-confirming. */
67
74
  const JOB_KILL_ARM_MS = 3000
75
+ /**
76
+ * How many of the LATEST rows stay visible by default before the earlier
77
+ * (history) rows collapse behind a toggle: {@link SUBAGENT_VISIBLE} for the
78
+ * topology's child rows, {@link JOBS_VISIBLE} for the background-job list.
79
+ * Collapsing is view-only — counts, the output dock and live observation
80
+ * keep seeing every entry.
81
+ */
82
+ const SUBAGENT_VISIBLE = 5
83
+ const JOBS_VISIBLE = 3
68
84
 
69
85
  /** The direct subagent children of one parent (durable `origin` rows;
70
86
  * Side Chat threads ride the same origin but are tab-strip conversations,
@@ -244,7 +260,12 @@ interface RowsProps {
244
260
  refresh: (parentSessionId: string) => void
245
261
  }
246
262
 
247
- /** Render one topology level; branches are always expanded (lazy catalogs). */
263
+ /**
264
+ * Render one topology level; branches are always expanded (lazy catalogs).
265
+ * When a level lists more than {@link SUBAGENT_VISIBLE} children, only the
266
+ * LATEST ones render by default — the earlier rows collapse behind a
267
+ * history toggle (per-level state; a fresh catalog page collapses again).
268
+ */
248
269
  function CatalogRows({
249
270
  parentSessionId, catalog, catalogs, byId, level, currentSessionId, live,
250
271
  openChild, refresh,
@@ -258,6 +279,10 @@ function CatalogRows({
258
279
  if (entry.kind === 'child') return !(entry.label?.startsWith(SIDE_LABEL_PREFIX) ?? false)
259
280
  return !(byId[entry.id]?.displayTitle.startsWith(SIDE_LABEL_PREFIX) ?? false)
260
281
  })
282
+ const [historyOpen, setHistoryOpen] = useState(false)
283
+ const historyCount = visibleEntries.length - SUBAGENT_VISIBLE
284
+ const collapsed = historyCount > 0 && !historyOpen
285
+ const renderEntries = collapsed ? visibleEntries.slice(-SUBAGENT_VISIBLE) : visibleEntries
261
286
  return (
262
287
  <>
263
288
  {emptyLoading && (
@@ -276,7 +301,18 @@ function CatalogRows({
276
301
  </button>
277
302
  </div>
278
303
  )}
279
- {visibleEntries.map((entry) => {
304
+ {historyCount > 0 && (
305
+ <button
306
+ type="button"
307
+ className={css.historyToggle}
308
+ aria-expanded={historyOpen}
309
+ onClick={() => { setHistoryOpen(open => !open) }}
310
+ >
311
+ {historyOpen ? <IconChevronDownOutline14 /> : <IconChevronRightOutline14 />}
312
+ {historyOpen ? t('subagentHideHistory') : t('subagentShowHistory', { count: historyCount })}
313
+ </button>
314
+ )}
315
+ {renderEntries.map((entry) => {
280
316
  if (entry.kind === 'diagnostic') {
281
317
  return (
282
318
  <div key={entry.id} className={css.subagentNode}>
@@ -467,10 +503,12 @@ function JobOutputPane(props: {
467
503
  /**
468
504
  * The background-job section of the Subagent page: every job of the whole
469
505
  * current tree (main agent + subagents, owner-labeled), fed by the harness
470
- * `session/jobs` push mirror. Clicking a row feeds its model-read output to
471
- * the shared bottom dock (event replay never the model's cursor); live
472
- * rows carry a two-click-confirm kill button. Renders nothing while the
473
- * tree has no jobs.
506
+ * `session/jobs` push mirror. When more than {@link JOBS_VISIBLE} jobs
507
+ * exist, only the head of the standard order (live rows, then newest
508
+ * settled) stays visible earlier rows collapse behind a history toggle.
509
+ * Clicking a row feeds its model-read output to the shared bottom dock
510
+ * (event replay — never the model's cursor); live rows carry a
511
+ * two-click-confirm kill button. Renders nothing while the tree has no jobs.
474
512
  */
475
513
  function JobsSection(props: {
476
514
  byId: SidebarSessionList['byId']
@@ -485,6 +523,7 @@ function JobsSection(props: {
485
523
  [byId, jobsBySession, rootId],
486
524
  )
487
525
  const [selectedId, setSelectedId] = useState<string | undefined>(undefined)
526
+ const [historyOpen, setHistoryOpen] = useState(false)
488
527
  const [armedId, setArmedId] = useState<string | undefined>(undefined)
489
528
  const [killingId, setKillingId] = useState<string | undefined>(undefined)
490
529
  const [killErrorId, setKillErrorId] = useState<string | undefined>(undefined)
@@ -547,6 +586,12 @@ function JobsSection(props: {
547
586
  ? t('jobsCountRunning', { count: rows.length, running: liveCount })
548
587
  : t('jobsCount', { count: rows.length })
549
588
 
589
+ // Collapsed view: the head of the standard order stays visible (all live
590
+ // rows up to the cap, then newest settled); the header count still
591
+ // reports the full totals, and a hidden job's dock keeps working.
592
+ const historyCount = rows.length - JOBS_VISIBLE
593
+ const visibleRows = historyCount > 0 && !historyOpen ? rows.slice(0, JOBS_VISIBLE) : rows
594
+
550
595
  return (
551
596
  <>
552
597
  <section className={css.jobs} aria-label={t('jobs')}>
@@ -555,7 +600,7 @@ function JobsSection(props: {
555
600
  <span className={css.jobsCount}>{countLabel}</span>
556
601
  </div>
557
602
  <ul className={css.jobsList} aria-label={t('jobs')}>
558
- {rows.map((row) => {
603
+ {visibleRows.map((row) => {
559
604
  const { job } = row
560
605
  const live = isJobLive(job)
561
606
  const selected = selectedId === job.id
@@ -617,6 +662,17 @@ function JobsSection(props: {
617
662
  )
618
663
  })}
619
664
  </ul>
665
+ {historyCount > 0 && (
666
+ <button
667
+ type="button"
668
+ className={css.historyToggle}
669
+ aria-expanded={historyOpen}
670
+ onClick={() => { setHistoryOpen(open => !open) }}
671
+ >
672
+ {historyOpen ? <IconChevronDownOutline14 /> : <IconChevronRightOutline14 />}
673
+ {historyOpen ? t('jobsHideHistory') : t('jobsShowHistory', { count: historyCount })}
674
+ </button>
675
+ )}
620
676
  </section>
621
677
  {selectedRow !== undefined && (
622
678
  <JobOutputPane
@@ -297,6 +297,8 @@ export const ar: Record<string, string> = {
297
297
  subagentDiagUnsupported: 'غير مدعوم',
298
298
  subagentDiagUnavailable: 'غير متاح',
299
299
  subagentThinking: 'جارٍ التفكير…',
300
+ subagentShowHistory: 'عرض {count} من الوكلاء الفرعيين الأقدم',
301
+ subagentHideHistory: 'طي الوكلاء الفرعيين الأقدم',
300
302
  sideChat: 'محادثة جانبية (تجريبية)',
301
303
  sideChatNew: 'خيط جديد',
302
304
  sideChatUntitled: 'خيط جديد',
@@ -324,6 +326,8 @@ export const ar: Record<string, string> = {
324
326
  jobs: 'المهام الخلفية',
325
327
  jobsCount: '{count} مهمة خلفية',
326
328
  jobsCountRunning: '{count} مهمة خلفية · {running} قيد التشغيل',
329
+ jobsShowHistory: 'عرض {count} من المهام الخلفية الأقدم',
330
+ jobsHideHistory: 'طي المهام الخلفية الأقدم',
327
331
  jobStatusRunning: 'قيد التشغيل',
328
332
  jobStatusStopping: 'جارٍ الإيقاف',
329
333
  jobStatusCompleted: 'اكتمل',
@@ -282,6 +282,8 @@ export const de: Record<string, string> = {
282
282
  subagentDiagUnsupported: 'Nicht unterstützt',
283
283
  subagentDiagUnavailable: 'Nicht verfügbar',
284
284
  subagentThinking: 'Denkt nach…',
285
+ subagentShowHistory: '{count} ältere Subagenten anzeigen',
286
+ subagentHideHistory: 'Ältere Subagenten einklappen',
285
287
  sideChat: 'Seitenchat (beta)',
286
288
  sideChatNew: 'Neuer Thread',
287
289
  sideChatUntitled: 'Neuer Thread',
@@ -309,6 +311,8 @@ export const de: Record<string, string> = {
309
311
  jobs: 'Hintergrundaufgaben',
310
312
  jobsCount: '{count} Hintergrundaufgaben',
311
313
  jobsCountRunning: '{count} Hintergrundaufgaben · {running} läuft',
314
+ jobsShowHistory: '{count} ältere Hintergrundaufgaben anzeigen',
315
+ jobsHideHistory: 'Ältere Hintergrundaufgaben einklappen',
312
316
  jobStatusRunning: 'Läuft',
313
317
  jobStatusStopping: 'Wird beendet',
314
318
  jobStatusCompleted: 'Abgeschlossen',
@@ -289,6 +289,8 @@ export const fr: Record<string, string> = {
289
289
  subagentDiagUnsupported: 'Entrée non prise en charge',
290
290
  subagentDiagUnavailable: 'Indisponible',
291
291
  subagentThinking: 'Réflexion…',
292
+ subagentShowHistory: 'Afficher {count} sous-agents plus anciens',
293
+ subagentHideHistory: 'Réduire les sous-agents plus anciens',
292
294
  sideChat: 'Discussion latérale (bêta)',
293
295
  sideChatNew: 'Nouvelle discussion',
294
296
  sideChatUntitled: 'Nouvelle discussion',
@@ -316,6 +318,8 @@ export const fr: Record<string, string> = {
316
318
  jobs: 'Tâches d’arrière-plan',
317
319
  jobsCount: '{count} tâche(s) d’arrière-plan',
318
320
  jobsCountRunning: '{count} tâche(s) d’arrière-plan · {running} en cours',
321
+ jobsShowHistory: 'Afficher {count} tâches d’arrière-plan plus anciennes',
322
+ jobsHideHistory: 'Réduire les tâches d’arrière-plan plus anciennes',
319
323
  jobStatusRunning: 'En cours',
320
324
  jobStatusStopping: 'Arrêt en cours',
321
325
  jobStatusCompleted: 'Terminée',
@@ -296,6 +296,8 @@ export const hi: Record<string, string> = {
296
296
  subagentDiagUnsupported: 'असमर्थित',
297
297
  subagentDiagUnavailable: 'अनुपलब्ध',
298
298
  subagentThinking: 'सोच रहा…',
299
+ subagentShowHistory: 'पुराने {count} सबएजेंट दिखाएँ',
300
+ subagentHideHistory: 'पुराने सबएजेंट छिपाएँ',
299
301
  sideChat: 'साइड चैट (बीटा)',
300
302
  sideChatNew: 'नया थ्रेड',
301
303
  sideChatUntitled: 'नया थ्रेड',
@@ -323,6 +325,8 @@ export const hi: Record<string, string> = {
323
325
  jobs: 'बैकग्राउंड कार्य',
324
326
  jobsCount: '{count} बैकग्राउंड कार्य',
325
327
  jobsCountRunning: '{count} बैकग्राउंड कार्य · {running} चल रहे',
328
+ jobsShowHistory: 'पुराने {count} बैकग्राउंड कार्य दिखाएँ',
329
+ jobsHideHistory: 'पुराने बैकग्राउंड कार्य छिपाएँ',
326
330
  jobStatusRunning: 'चल रहा',
327
331
  jobStatusStopping: 'रुक रहा',
328
332
  jobStatusCompleted: 'पूर्ण',
@@ -294,6 +294,8 @@ export const id: Record<string, string> = {
294
294
  subagentDiagUnsupported: 'Tidak didukung',
295
295
  subagentDiagUnavailable: 'Tidak tersedia',
296
296
  subagentThinking: 'Berpikir…',
297
+ subagentShowHistory: 'Tampilkan {count} subagen sebelumnya',
298
+ subagentHideHistory: 'Ciutkan subagen sebelumnya',
297
299
  sideChat: 'Side Chat (beta)',
298
300
  sideChatNew: 'Thread baru',
299
301
  sideChatUntitled: 'Thread baru',
@@ -321,6 +323,8 @@ export const id: Record<string, string> = {
321
323
  jobs: 'Tugas latar',
322
324
  jobsCount: '{count} tugas latar',
323
325
  jobsCountRunning: '{count} tugas latar · {running} berjalan',
326
+ jobsShowHistory: 'Tampilkan {count} tugas latar sebelumnya',
327
+ jobsHideHistory: 'Ciutkan tugas latar sebelumnya',
324
328
  jobStatusRunning: 'Berjalan',
325
329
  jobStatusStopping: 'Menghentikan',
326
330
  jobStatusCompleted: 'Selesai',
@@ -287,6 +287,8 @@ export const it: Record<string, string> = {
287
287
  subagentDiagUnsupported: 'Non supportato',
288
288
  subagentDiagUnavailable: 'Non disponibile',
289
289
  subagentThinking: 'In pensiero…',
290
+ subagentShowHistory: 'Mostra {count} sottoagenti precedenti',
291
+ subagentHideHistory: 'Comprimi sottoagenti precedenti',
290
292
  sideChat: 'Chat laterale (beta)',
291
293
  sideChatNew: 'Nuova conversazione',
292
294
  sideChatUntitled: 'Nuova conversazione',
@@ -314,6 +316,8 @@ export const it: Record<string, string> = {
314
316
  jobs: 'Attività in background',
315
317
  jobsCount: '{count} attività in background',
316
318
  jobsCountRunning: '{count} attività in background · {running} in esecuzione',
319
+ jobsShowHistory: 'Mostra {count} attività in background precedenti',
320
+ jobsHideHistory: 'Comprimi attività in background precedenti',
317
321
  jobStatusRunning: 'In esecuzione',
318
322
  jobStatusStopping: 'Arresto in corso',
319
323
  jobStatusCompleted: 'Completata',