pikiloom 0.4.26 → 0.4.28

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 (156) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
  10. package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
  11. package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
  12. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  13. package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
  153. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  154. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  155. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  156. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,7 +1,3 @@
1
- /**
2
- * Pure utility functions shared across the agent layer.
3
- * No filesystem or session state side effects.
4
- */
5
1
  import fs from 'node:fs';
6
2
  import { writeScopedLog } from '../core/logging.js';
7
3
  export const Q = (a) => {
@@ -70,7 +66,6 @@ export function normalizeStreamPreviewPlan(value) {
70
66
  steps,
71
67
  };
72
68
  }
73
- /** Parse a TodoWrite tool input into a StreamPreviewPlan. */
74
69
  export function parseTodoWriteAsPlan(input) {
75
70
  if (!input || typeof input !== 'object')
76
71
  return null;
@@ -95,11 +90,6 @@ export function parseTodoWriteAsPlan(input) {
95
90
  return { explanation: null, steps };
96
91
  }
97
92
  export function normalizeActivityLine(text) { return text.replace(/\s+/g, ' ').trim(); }
98
- // The activity feed is only ever rendered as a tail — downstream previews trim
99
- // it to ~900 chars (trimActivityForPreview) and the final reply to ~1600 — yet
100
- // every tool event rebuilds `s.activity = recentActivity.join('\n')`. A 500-line
101
- // cap made that rejoin effectively O(n²) over a tool-heavy turn for history no
102
- // view ever shows; 80 lines comfortably covers the largest consumer.
103
93
  export function pushRecentActivity(lines, line, maxLines = 80) {
104
94
  const cleaned = normalizeActivityLine(line);
105
95
  if (!cleaned)
@@ -118,10 +108,6 @@ export function firstNonEmptyLine(text) {
118
108
  }
119
109
  return '';
120
110
  }
121
- // MCP tool results carry structured content blocks (e.g. screenshot returns
122
- // `[{type:'image',...}, {type:'text', text:'Saved as ...'}]`). Coerce that to
123
- // plain text by keeping only the `type:'text'` blocks; otherwise `String([{…}])`
124
- // silently becomes the literal "[object Object]" in activity summaries.
125
111
  export function coerceToolResultText(value) {
126
112
  if (typeof value === 'string')
127
113
  return value;
@@ -171,24 +157,6 @@ export function joinErrorMessages(errors) {
171
157
  return '';
172
158
  return errors.map(error => normalizeErrorMessage(error)).filter(Boolean).join('; ').trim();
173
159
  }
174
- /**
175
- * Detect Claude Code's synthetic "API Error: …" assistant message. When the
176
- * upstream Anthropic API returns a transient error (529 Overloaded, 5xx, gateway
177
- * timeouts, …), the Claude CLI swallows it and replaces the assistant turn with
178
- * a single `text` block whose body is literally `API Error: <reason>`. The
179
- * turn's stop_reason still claims `end_turn`, so the driver can't distinguish
180
- * it from a normal short reply without inspecting the text.
181
- *
182
- * Heuristics — keep them tight so real prose mentioning "API Error" doesn't
183
- * trip the detector:
184
- * - exact prefix "API Error: "
185
- * - total length ≤ 200 chars (the synthetic line is always short)
186
- * - no newlines (legit prose containing "API Error" virtually always wraps)
187
- *
188
- * Returns the trimmed reason (e.g. "Overloaded", "Internal server error") when
189
- * matched, otherwise null. Callers decide whether the reason is retryable —
190
- * `looksRetryable` answers that.
191
- */
192
160
  export function detectClaudeApiError(text) {
193
161
  if (!text)
194
162
  return null;
@@ -198,36 +166,12 @@ export function detectClaudeApiError(text) {
198
166
  const m = trimmed.match(/^API Error:\s*(.+)$/i);
199
167
  return m ? m[1].trim() : null;
200
168
  }
201
- /**
202
- * Retryable Claude Code API errors — transient upstream conditions that
203
- * usually clear within seconds. Non-retryable conditions (auth, quota,
204
- * context length) fall through and surface to the user immediately.
205
- */
206
169
  export function isRetryableClaudeApiError(reason) {
207
170
  const r = reason.toLowerCase();
208
171
  if (/rate limit|rate limited|quota|usage limit|session limit/i.test(r))
209
172
  return false;
210
173
  return /overloaded|overload|timeout|timed out|500|502|503|504|529|temporar|gateway|connection|network|internal (server )?error/i.test(r);
211
174
  }
212
- /**
213
- * Detect Claude Code's "selected model is unavailable" notice — emitted when
214
- * the requested `--model` id is disabled / not provisioned for the account (a
215
- * 404 model_not_found). Its delivery differs by mode:
216
- * - `-p`/stream-json: a `<synthetic>` assistant event carrying
217
- * `error:"model_not_found"` plus the banner text, and a `result` event with
218
- * `is_error` — both reach the parser.
219
- * - TUI: the banner is *only* painted to the PTY screen. It is never written
220
- * to the transcript JSONL and fires no Stop hook (verified 2026-06-13), so
221
- * the screen scrape is the sole signal and the turn would otherwise hang
222
- * until the stall watchdog (3–10 min).
223
- *
224
- * Matching is whitespace-insensitive on purpose: the TUI renders the banner
225
- * character-by-character with cursor positioning, so after ANSI stripping the
226
- * words lose their spaces and wrap arbitrarily ("issuewiththeselectedmodel").
227
- * Collapsing whitespace on both sides makes the match survive that rendering.
228
- * Callers compose the user-facing message via `claudeModelErrorMessage` with
229
- * the concrete model id they hold.
230
- */
231
175
  export function detectClaudeModelError(text) {
232
176
  if (!text)
233
177
  return false;
@@ -235,7 +179,6 @@ export function detectClaudeModelError(text) {
235
179
  return collapsed.includes('issuewiththeselectedmodel')
236
180
  || collapsed.includes('maynotexistoryoumaynothaveaccess');
237
181
  }
238
- /** User-facing message for an unavailable / no-access model (see {@link detectClaudeModelError}). */
239
182
  export function claudeModelErrorMessage(model) {
240
183
  const id = (model || '').trim();
241
184
  return `The selected model${id ? ` (${id})` : ''} is unavailable — it may not exist, or this account doesn't have access to it. Switch to a different model in pikiloom settings.`;
@@ -267,7 +210,6 @@ export function computeContext(s) {
267
210
  : null;
268
211
  return { contextUsedTokens: used, contextPercent: pct };
269
212
  }
270
- /** Max structured tool calls carried per preview emit (most recent win). */
271
213
  const PREVIEW_TOOL_CALLS_MAX = 40;
272
214
  export function buildStreamPreviewMeta(s) {
273
215
  const ctx = computeContext(s);
@@ -276,7 +218,6 @@ export function buildStreamPreviewMeta(s) {
276
218
  cachedInputTokens: s.cachedInputTokens,
277
219
  contextUsedTokens: ctx.contextUsedTokens, contextPercent: ctx.contextPercent,
278
220
  };
279
- // Turn-cumulative output: finished calls' total + the in-flight call.
280
221
  const turnOutput = (s.turnOutputTokensBase ?? 0) + (s.outputTokens ?? 0);
281
222
  if (turnOutput > 0)
282
223
  meta.turnOutputTokens = turnOutput;
@@ -306,12 +247,6 @@ export function buildStreamPreviewMeta(s) {
306
247
  }
307
248
  return meta;
308
249
  }
309
- /**
310
- * Bounded, human-readable input detail for a live tool-call row. Bash shows
311
- * the raw command (the summary already carries the description); everything
312
- * else gets compact JSON. Returns null when there's nothing beyond the
313
- * summary worth expanding.
314
- */
315
250
  export function previewToolCallInput(name, input, max = 500) {
316
251
  if (input == null)
317
252
  return null;
@@ -329,11 +264,6 @@ export function previewToolCallInput(name, input, max = 500) {
329
264
  return null;
330
265
  }
331
266
  }
332
- /**
333
- * Bounded text preview of a tool result. Accepts the JSONL tool_result
334
- * `content` (string | block array) or a hook `tool_response` (string |
335
- * object). Extracts text blocks where present, falls back to compact JSON.
336
- */
337
267
  export function previewToolCallResult(content, max = 500) {
338
268
  if (content == null)
339
269
  return null;
@@ -348,7 +278,6 @@ export function previewToolCallResult(content, max = 500) {
348
278
  return text ? clipText(text, max) : null;
349
279
  }
350
280
  if (typeof content === 'object') {
351
- // Hook tool_response commonly nests the payload under `content` / `result`.
352
281
  if (content.content != null && content.content !== content) {
353
282
  const nested = previewToolCallResult(content.content, max);
354
283
  if (nested)
@@ -373,7 +302,6 @@ function clipText(text, max) {
373
302
  return text;
374
303
  return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
375
304
  }
376
- // Claude tool use helpers (used by driver-claude.ts)
377
305
  export function summarizeClaudeToolUse(name, input) {
378
306
  const tool = String(name || '').trim() || 'Tool';
379
307
  const description = shortValue(input?.description, 120);
@@ -408,10 +336,6 @@ export function summarizeClaudeToolUse(name, input) {
408
336
  }
409
337
  case 'TodoWrite': return 'Update plan';
410
338
  case 'AskUserQuestion': {
411
- // Claude's built-in clarify tool. The CLI in `-p` mode self-resolves it
412
- // with an error and degrades to a plain-text question in the same turn —
413
- // we just surface the question text in the activity panel so users see
414
- // what was asked.
415
339
  const qs = Array.isArray(input?.questions) ? input.questions : [];
416
340
  const first = qs[0];
417
341
  const q = shortValue(first?.question || input?.question, 120);
@@ -428,7 +352,6 @@ export function summarizeClaudeToolUse(name, input) {
428
352
  return c ? `Run shell: ${c}` : 'Run shell command';
429
353
  }
430
354
  default: {
431
- // MCP tools come through as `mcp__<server>__<tool>` — unwrap common pikiloom tools
432
355
  const mcpMatch = tool.match(/^mcp__[^_]+__(.+)$/);
433
356
  const bare = mcpMatch ? mcpMatch[1] : tool;
434
357
  if (bare === 'im_send_file') {
@@ -464,7 +387,6 @@ export function summarizeClaudeToolResult(tool, block, toolUseResult) {
464
387
  return `${summary} done`;
465
388
  return `${summary} -> ${shortValue(detail, 120)}`;
466
389
  }
467
- // Usage helpers (used by drivers)
468
390
  export function roundPercent(value) {
469
391
  const n = Number(value);
470
392
  if (!Number.isFinite(n))
@@ -580,7 +502,6 @@ export function stripInjectedPrompts(text) {
580
502
  if (idx >= 0)
581
503
  text = text.slice(0, idx).trim();
582
504
  }
583
- // Strip Codex IDE context prefix ("# Context from my IDE setup: ...")
584
505
  if (text.startsWith('# Context from')) {
585
506
  const tag = '## My request for Codex:\n';
586
507
  const idx = text.indexOf(tag);
@@ -595,18 +516,7 @@ export const SESSION_PREVIEW_IGNORED_USER_PATTERNS = [
595
516
  ];
596
517
  export const SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE = /\[Image:[^\]]+\]/gi;
597
518
  export const SESSION_PREVIEW_FILE_PLACEHOLDER_RE = /\[Attached file:[^\]]+\]/gi;
598
- /**
599
- * Claude TUI mode prepends `@/abs/path/file.ext` mentions to the prompt as
600
- * positional argv (see `src/agent/drivers/claude-tui.ts`) — that's how the TUI
601
- * ingests local image files. The mentions end up baked into the JSONL user
602
- * `content` string verbatim. This regex captures them so:
603
- * - the dashboard's user bubble (via `getClaudeSessionMessages`) can lift
604
- * them into structured `image` blocks for thumbnail rendering;
605
- * - session-list previews don't surface a long absolute path.
606
- * Whitespace-free paths only — matches what `claude-tui.ts` emits.
607
- */
608
519
  export const CLAUDE_AT_MENTION_IMAGE_RE = /(^|\s)@(\/[^\s@\n]+\.(?:png|jpe?g|gif|webp|svg))(?=\s|$)/gi;
609
- /** Pull the absolute paths out of every image-mention in `text`. */
610
520
  export function extractClaudeAtMentionImagePaths(text) {
611
521
  if (!text)
612
522
  return [];
@@ -615,8 +525,6 @@ export function extractClaudeAtMentionImagePaths(text) {
615
525
  out.push(m[2]);
616
526
  return out;
617
527
  }
618
- /** Remove image @-mentions from `text` while preserving the leading boundary
619
- * character (start-of-string or whitespace) so adjacent content stays joinable. */
620
528
  export function stripClaudeAtMentionImages(text) {
621
529
  if (!text)
622
530
  return text;
@@ -638,18 +546,6 @@ export function sanitizeSessionUserPreviewText(text) {
638
546
  export function isPendingSessionId(sessionId) {
639
547
  return typeof sessionId === 'string' && sessionId.startsWith('pending_');
640
548
  }
641
- /**
642
- * Update a stream-state's session id and notify the caller in one step.
643
- *
644
- * Drivers used to assign `s.sessionId = ev.session_id ?? s.sessionId` at every
645
- * place the CLI surfaced an id, then leave promotion until `finalizeStreamResult`
646
- * at end-of-stream. That meant an early abort (before the result line) or a
647
- * mid-stream rotation (Claude `--resume` rewriting the session id) was invisible
648
- * to the bot runtime — leaving the runtime stuck on a pending id, or letting a
649
- * later insertion land on a phantom session. Routing through this helper makes
650
- * every observed id change propagate immediately to `opts.onSessionId`, which
651
- * in bot.ts wires straight into `promoteSessionRuntime`.
652
- */
653
549
  export function emitSessionIdUpdate(s, rawId) {
654
550
  if (typeof rawId !== 'string')
655
551
  return;
@@ -660,22 +556,8 @@ export function emitSessionIdUpdate(s, rawId) {
660
556
  try {
661
557
  s._emitSessionId?.(trimmed);
662
558
  }
663
- catch { /* listeners must not break the stream loop */ }
664
- }
665
- /**
666
- * Canonical session-list display title used by *every* surface (IM channels
667
- * + dashboard). The order is intentional:
668
- *
669
- * 1. `title` — set ONCE from the original user prompt that started the
670
- * session. Stable; never overwritten by sub-agent or tool prompts.
671
- * 2. `lastQuestion` — most recent user message. Fallback only, because for
672
- * Claude this can be a Task-tool sub-agent prompt and we don't want
673
- * sub-agent text leaking into the title.
674
- * 3. `sessionId` — last-resort identifier.
675
- *
676
- * The dashboard frontend (`dashboard/src/utils.ts`) mirrors this order — keep
677
- * the two in sync.
678
- */
559
+ catch { }
560
+ }
679
561
  export function sessionListDisplayTitle(session) {
680
562
  const title = sanitizeSessionUserPreviewText(String(session.title || ''));
681
563
  if (title)