pikiloom 0.4.16 → 0.4.17

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 (36) hide show
  1. package/README.md +2 -1
  2. package/dashboard/dist/assets/{AgentTab-B5tmLxa7.js → AgentTab-CDVhy5K1.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-CBp5nyfS.js → DirBrowser-BElI1-4D.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-w4pkrNas.js → ExtensionsTab-BB8ipJ77.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-37Po5LP1.js → IMAccessTab-IZt_yXoG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-CBMO5UcS.js → Modal-C1EAGSL1.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-DMlEjJUG.js → Modals-DYUV5yR9.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BiSTkS_t.js → Select-BnsbE6Qv.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-Ca_TVTT1.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-Brzt5wTT.js → SystemTab-Dk6k2OTt.js} +1 -1
  11. package/dashboard/dist/assets/{index-5Q-Q7ByM.js → index-CK-3CNRp.js} +2 -2
  12. package/dashboard/dist/assets/index-CnJsD381.js +23 -0
  13. package/dashboard/dist/assets/index-dzfjF9Js.css +1 -0
  14. package/dashboard/dist/assets/{shared-P-W1OYQ6.js → shared-CZVD0MJD.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/artifacts.js +160 -0
  17. package/dist/agent/images.js +51 -24
  18. package/dist/agent/index.js +4 -2
  19. package/dist/agent/mcp/tools/workspace.js +4 -3
  20. package/dist/bot/bot.js +83 -4
  21. package/dist/bot/commands.js +48 -2
  22. package/dist/bot/menu.js +1 -0
  23. package/dist/bot/session-hub.js +1 -1
  24. package/dist/channels/dingtalk/bot.js +9 -1
  25. package/dist/channels/discord/bot.js +9 -1
  26. package/dist/channels/feishu/bot.js +8 -1
  27. package/dist/channels/slack/bot.js +9 -1
  28. package/dist/channels/telegram/bot.js +8 -1
  29. package/dist/channels/wecom/bot.js +9 -1
  30. package/dist/channels/weixin/bot.js +9 -1
  31. package/dist/cli/main.js +1 -0
  32. package/dist/dashboard/routes/sessions.js +108 -27
  33. package/package.json +1 -1
  34. package/dashboard/dist/assets/SessionPanel-BVC7kwlX.js +0 -1
  35. package/dashboard/dist/assets/index-Dw3ty4QY.js +0 -23
  36. package/dashboard/dist/assets/index-FD86DEDF.css +0 -1
@@ -13,7 +13,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orches
13
13
  import { shutdownAllDrivers } from '../../agent/driver.js';
14
14
  import { expandTilde } from '../../core/platform.js';
15
15
  import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
16
- import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, } from '../../bot/commands.js';
16
+ import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getSessionsDigestData, formatSessionsDigestText, getStartData, getWorkspacesData, } from '../../bot/commands.js';
17
17
  import { SlackChannel } from './channel.js';
18
18
  import { getActiveUserConfig } from '../../core/config/user-config.js';
19
19
  const SHUTDOWN_EXIT_CODE = {
@@ -131,6 +131,7 @@ export class SlackBot extends Bot {
131
131
  '/switch [path] - Change workdir',
132
132
  '/workspaces [#] - Pick saved workspace',
133
133
  '/sessions [new|#] - List/switch sessions',
134
+ '/digest - Recent session digest',
134
135
  '/skills - List project skills',
135
136
  '/stop - Stop current task',
136
137
  '/restart - Restart pikiloom',
@@ -165,6 +166,9 @@ export class SlackBot extends Bot {
165
166
  case 'sessions':
166
167
  await this.cmdSessions(ctx, args);
167
168
  return true;
169
+ case 'digest':
170
+ await this.cmdDigest(ctx);
171
+ return true;
168
172
  case 'skills':
169
173
  await this.cmdSkills(ctx);
170
174
  return true;
@@ -192,6 +196,10 @@ export class SlackBot extends Bot {
192
196
  lines.push('', 'Ready. Send a message to start.');
193
197
  await ctx.reply(lines.join('\n'));
194
198
  }
199
+ async cmdDigest(ctx) {
200
+ const data = await getSessionsDigestData(this, ctx.chatId);
201
+ await ctx.reply(formatSessionsDigestText(data));
202
+ }
195
203
  async cmdStatus(ctx) {
196
204
  const d = await getStatusDataAsync(this, ctx.chatId);
197
205
  const gitLine = formatGitStatusLine(d.git);
@@ -14,7 +14,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, SessionMessageRegistry, buildBotMenuState,
14
14
  import { stageSessionFiles, } from '../../agent/index.js';
15
15
  import { shutdownAllDrivers } from '../../agent/driver.js';
16
16
  import { SKILL_CMD_PREFIX, } from '../../bot/menu.js';
17
- import { getStartData, getStatusDataAsync, getHostDataSync, getSessionTurnPreviewData, getWorkspacesData, resolveSkillPrompt, summarizePromptForStatus, handleGoalCommand, } from '../../bot/commands.js';
17
+ import { getStartData, getStatusDataAsync, getHostDataSync, getSessionTurnPreviewData, getWorkspacesData, resolveSkillPrompt, summarizePromptForStatus, handleGoalCommand, getSessionsDigestData, formatSessionsDigestText, } from '../../bot/commands.js';
18
18
  import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
19
19
  import { buildSwitchWorkdirView, buildWorkspacesView, resolveRegisteredPath } from './directory.js';
20
20
  import { LivePreview } from './live-preview.js';
@@ -353,6 +353,10 @@ export class TelegramBot extends Bot {
353
353
  async cmdSessions(ctx) {
354
354
  await this.sendCommandView(ctx, await buildSessionsCommandView(this, ctx.chatId, 0, this.sessionsPageSize));
355
355
  }
356
+ async cmdDigest(ctx) {
357
+ const data = await getSessionsDigestData(this, ctx.chatId);
358
+ await ctx.reply(formatSessionsDigestText(data));
359
+ }
356
360
  async cmdStatus(ctx) {
357
361
  const d = await getStatusDataAsync(this, ctx.chatId);
358
362
  const gitLine = formatGitStatusLine(d.git);
@@ -1167,6 +1171,9 @@ export class TelegramBot extends Bot {
1167
1171
  case 'sessions':
1168
1172
  await this.cmdSessions(ctx);
1169
1173
  return;
1174
+ case 'digest':
1175
+ await this.cmdDigest(ctx);
1176
+ return;
1170
1177
  case 'agents':
1171
1178
  await this.cmdAgents(ctx);
1172
1179
  return;
@@ -9,7 +9,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orches
9
9
  import { shutdownAllDrivers } from '../../agent/driver.js';
10
10
  import { expandTilde } from '../../core/platform.js';
11
11
  import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
12
- import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, } from '../../bot/commands.js';
12
+ import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getSessionsDigestData, formatSessionsDigestText, getStartData, getWorkspacesData, } from '../../bot/commands.js';
13
13
  import { WeComChannel } from './channel.js';
14
14
  import { getActiveUserConfig } from '../../core/config/user-config.js';
15
15
  const SHUTDOWN_EXIT_CODE = {
@@ -135,6 +135,7 @@ export class WeComBot extends Bot {
135
135
  '/switch [path] - Change workdir',
136
136
  '/workspaces [#] - Pick saved workspace',
137
137
  '/sessions [new|#] - List/switch sessions',
138
+ '/digest - Recent session digest',
138
139
  '/skills - List project skills',
139
140
  '/stop - Stop current task',
140
141
  '/restart - Restart pikiloom',
@@ -168,6 +169,9 @@ export class WeComBot extends Bot {
168
169
  case 'sessions':
169
170
  await this.cmdSessions(ctx, args);
170
171
  return true;
172
+ case 'digest':
173
+ await this.cmdDigest(ctx);
174
+ return true;
171
175
  case 'skills':
172
176
  await this.cmdSkills(ctx);
173
177
  return true;
@@ -195,6 +199,10 @@ export class WeComBot extends Bot {
195
199
  lines.push('', 'Ready. Send a message to start.');
196
200
  await ctx.reply(lines.join('\n'));
197
201
  }
202
+ async cmdDigest(ctx) {
203
+ const data = await getSessionsDigestData(this, ctx.chatId);
204
+ await ctx.reply(formatSessionsDigestText(data));
205
+ }
198
206
  async cmdStatus(ctx) {
199
207
  const d = await getStatusDataAsync(this, ctx.chatId);
200
208
  const gitLine = formatGitStatusLine(d.git);
@@ -11,7 +11,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orches
11
11
  import { shutdownAllDrivers } from '../../agent/driver.js';
12
12
  import { expandTilde } from '../../core/platform.js';
13
13
  import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
14
- import { getStatusDataAsync, getHostDataSync, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, handleGoalCommand, } from '../../bot/commands.js';
14
+ import { getStatusDataAsync, getHostDataSync, getModelsListData, getSessionsPageData, getSessionsDigestData, formatSessionsDigestText, getStartData, getWorkspacesData, handleGoalCommand, } from '../../bot/commands.js';
15
15
  import { WeixinChannel } from './channel.js';
16
16
  import { getActiveUserConfig } from '../../core/config/user-config.js';
17
17
  const SHUTDOWN_EXIT_CODE = {
@@ -175,6 +175,7 @@ export class WeixinBot extends Bot {
175
175
  '/switch [path] - Change workdir',
176
176
  '/workspaces [#] - Pick saved workspace',
177
177
  '/sessions [new|#] - List/switch sessions',
178
+ '/digest - Recent session digest',
178
179
  '/skills - List & run project skills',
179
180
  '/cancel - Cancel an active interactive prompt',
180
181
  '/stop - Stop current task',
@@ -210,6 +211,9 @@ export class WeixinBot extends Bot {
210
211
  case 'sessions':
211
212
  await this.cmdSessions(ctx, args);
212
213
  return true;
214
+ case 'digest':
215
+ await this.cmdDigest(ctx);
216
+ return true;
213
217
  case 'skills':
214
218
  await this.cmdSkills(ctx);
215
219
  return true;
@@ -241,6 +245,10 @@ export class WeixinBot extends Bot {
241
245
  lines.push('', 'Ready. Send a message to start.');
242
246
  await ctx.reply(lines.join('\n'));
243
247
  }
248
+ async cmdDigest(ctx) {
249
+ const data = await getSessionsDigestData(this, ctx.chatId);
250
+ await ctx.reply(formatSessionsDigestText(data));
251
+ }
244
252
  async cmdStatus(ctx) {
245
253
  const d = await getStatusDataAsync(this, ctx.chatId);
246
254
  const gitLine = formatGitStatusLine(d.git);
package/dist/cli/main.js CHANGED
@@ -292,6 +292,7 @@ Environment variables (per agent):
292
292
 
293
293
  Bot commands (available once running):
294
294
  /sessions List or switch coding sessions
295
+ /digest Show a compact digest of recent sessions
295
296
  /agents List or switch AI agents
296
297
  /models List or switch models
297
298
  /status Bot status, uptime, and token usage
@@ -5,8 +5,9 @@ import { Hono } from 'hono';
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
+ import { Readable } from 'node:stream';
8
9
  import { loadUserConfig } from '../../core/config/user-config.js';
9
- import { listAgents, listSkills, decodeAttachmentPathParam, resolveAllowedAttachmentPath, rewriteImageBlocksForTransport, } from '../../agent/index.js';
10
+ import { listAgents, listSkills, decodeAttachmentPathParam, resolveAllowedAttachmentPath, rewriteAttachmentBlocksForTransport, deliveredArtifactBlocks, mimeForArtifact, } from '../../agent/index.js';
10
11
  import { getSessionStatusForBot } from '../../bot/session-status.js';
11
12
  import { findPikiloomSession } from '../../agent/session.js';
12
13
  import { readAwaitResume } from '../../agent/await-resume.js';
@@ -425,26 +426,56 @@ app.post('/api/session-hub/session/messages', async (c) => {
425
426
  turnLimit: Number.isFinite(turnLimit) ? turnLimit : undefined,
426
427
  rich,
427
428
  });
428
- return c.json(rewriteSessionImagesForDashboard(result, agent, sessionId));
429
+ return c.json(prepareSessionMessagesForDashboard(result, agent, sessionId));
429
430
  }
430
431
  catch (e) {
431
432
  return c.json({ ok: false, error: e.message }, 500);
432
433
  }
433
434
  });
434
- // Rewrite oversized inline image data URLs into attachment HTTP URLs so
435
- // dashboard JSON payloads stay compact. Small inline images pass through.
436
- function rewriteSessionImagesForDashboard(result, agent, sessionId) {
437
- if (!result.richMessages?.length)
435
+ // Prepare a session message read for the dashboard:
436
+ // 1. Rewrite on-disk image/file blocks into compact attachment HTTP URLs so a
437
+ // remote browser can fetch the bytes (inline data: images pass through).
438
+ // 2. Append the session's delivered artifacts (files the agent handed the
439
+ // user via `im_send_file`) as a trailing assistant message, so they render
440
+ // and stay retrievable after a reload regardless of which terminal
441
+ // delivered them. Only added when this window includes the conversation
442
+ // tail (`!hasNewer`) to avoid duplicating across paginated reads.
443
+ function prepareSessionMessagesForDashboard(result, agent, sessionId) {
444
+ // Only operate in rich mode — a `rich:false` read returns plain text and must
445
+ // not gain a synthetic richMessages array.
446
+ if (result.richMessages === undefined)
438
447
  return result;
439
448
  const richMessages = result.richMessages.map(message => ({
440
449
  ...message,
441
- blocks: rewriteImageBlocksForTransport(message.blocks, { agent, sessionId }),
450
+ blocks: rewriteAttachmentBlocksForTransport(message.blocks, { agent, sessionId }),
442
451
  }));
452
+ const includesTail = !result.window || !result.window.hasNewer;
453
+ if (includesTail) {
454
+ const delivered = rewriteAttachmentBlocksForTransport(deliveredArtifactBlocks(agent, sessionId), { agent, sessionId });
455
+ if (delivered.length) {
456
+ const text = deliveredSummaryText(delivered);
457
+ richMessages.push({ role: 'assistant', text, blocks: delivered });
458
+ }
459
+ }
460
+ if (!richMessages.length)
461
+ return result;
443
462
  return { ...result, richMessages };
444
463
  }
445
- // Attachment endpoint serves on-disk images referenced by RichMessage image
446
- // blocks via opaque base64url path tokens. The allowlist (see images.ts)
447
- // confines reads to a known set of agent-managed dirs + the session's workdir.
464
+ /** Plain-text fallback for the delivered-artifacts message (IM tail / exports). */
465
+ function deliveredSummaryText(blocks) {
466
+ const names = blocks
467
+ .map(b => b.fileName || (b.type === 'image' ? 'image' : 'file'))
468
+ .filter(Boolean);
469
+ if (!names.length)
470
+ return '';
471
+ return names.length === 1 ? `Delivered: ${names[0]}` : `Delivered ${names.length} files: ${names.join(', ')}`;
472
+ }
473
+ // Attachment endpoint — serves on-disk images AND delivered files referenced by
474
+ // RichMessage image/file blocks via opaque base64url path tokens. The allowlist
475
+ // (see images.ts) confines reads to a known set of agent-managed dirs + the
476
+ // session's workdir + the per-session delivered-artifacts dir. Streams the
477
+ // bytes (no full-buffer) and supports Range so large artifacts (video,
478
+ // archives) download/seek without pinning memory.
448
479
  app.get('/api/sessions/:agent/:id/attachment', async (c) => {
449
480
  const agent = c.req.param('agent');
450
481
  const sessionId = decodeURIComponent(c.req.param('id'));
@@ -491,28 +522,78 @@ app.get('/api/sessions/:agent/:id/attachment', async (c) => {
491
522
  }
492
523
  if (!stat.isFile())
493
524
  return c.json({ ok: false, error: 'not a file' }, 400);
494
- const ext = path.extname(resolved).toLowerCase();
495
- const mime = mimeForExtFallback(ext);
496
- const bytes = await fs.promises.readFile(resolved);
497
- // The path is hash-immutable for agent-managed dirs (`ig_<sha>.png`, …) and
498
- // the session lifecycle keeps the file stable — long cache is safe.
499
- return c.body(bytes, 200, {
525
+ const mime = mimeForArtifact(resolved);
526
+ const downloadName = sanitizeDownloadName(c.req.query('n'), resolved);
527
+ // Inline images so <img> renders them; everything else downloads with its
528
+ // pristine name. RFC 5987 filename* carries non-ASCII names safely.
529
+ const disposition = mime.startsWith('image/') ? 'inline' : 'attachment';
530
+ const asciiName = downloadName.replace(/[^\x20-\x7e]/g, '_').replace(/"/g, "'");
531
+ const contentDisposition = `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(downloadName)}`;
532
+ // The path is stamp-unique for delivered artifacts and hash-immutable for
533
+ // agent-managed dirs (`ig_<sha>.png`, …); the session lifecycle keeps the
534
+ // file stable — long cache is safe.
535
+ const baseHeaders = {
500
536
  'Content-Type': mime,
501
- 'Content-Length': String(bytes.length),
537
+ 'Content-Disposition': contentDisposition,
502
538
  'Cache-Control': 'private, max-age=31536000, immutable',
503
539
  'X-Content-Type-Options': 'nosniff',
540
+ 'Accept-Ranges': 'bytes',
541
+ };
542
+ // Honor a single byte-range request (e.g. video seek / resumable download).
543
+ const range = parseByteRange(c.req.header('range'), stat.size);
544
+ if (range) {
545
+ const nodeStream = fs.createReadStream(resolved, { start: range.start, end: range.end });
546
+ return c.body(Readable.toWeb(nodeStream), 206, {
547
+ ...baseHeaders,
548
+ 'Content-Range': `bytes ${range.start}-${range.end}/${stat.size}`,
549
+ 'Content-Length': String(range.end - range.start + 1),
550
+ });
551
+ }
552
+ const nodeStream = fs.createReadStream(resolved);
553
+ return c.body(Readable.toWeb(nodeStream), 200, {
554
+ ...baseHeaders,
555
+ 'Content-Length': String(stat.size),
504
556
  });
505
557
  });
506
- function mimeForExtFallback(ext) {
507
- switch (ext.toLowerCase()) {
508
- case '.png': return 'image/png';
509
- case '.jpg':
510
- case '.jpeg': return 'image/jpeg';
511
- case '.gif': return 'image/gif';
512
- case '.webp': return 'image/webp';
513
- case '.svg': return 'image/svg+xml';
514
- default: return 'application/octet-stream';
515
- }
558
+ /** Sanitize the optional `&n=` download-name hint; fall back to the basename. */
559
+ function sanitizeDownloadName(raw, resolved) {
560
+ const candidate = (raw || '').trim();
561
+ const name = candidate || path.basename(resolved);
562
+ // Strip path separators / control chars; the on-disk path is already
563
+ // validated `n` only affects the Content-Disposition filename.
564
+ return name.replace(/[/\\\0\r\n]+/g, '_').replace(/^\.+/, '').slice(0, 200) || 'download';
565
+ }
566
+ /** Parse a single `bytes=start-end` range header against `size`; null if absent
567
+ * or unsatisfiable (caller then serves the full body). */
568
+ function parseByteRange(header, size) {
569
+ if (!header || size <= 0)
570
+ return null;
571
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
572
+ if (!m)
573
+ return null;
574
+ const hasStart = m[1] !== '';
575
+ const hasEnd = m[2] !== '';
576
+ let start;
577
+ let end;
578
+ if (hasStart) {
579
+ start = parseInt(m[1], 10);
580
+ end = hasEnd ? parseInt(m[2], 10) : size - 1;
581
+ }
582
+ else if (hasEnd) {
583
+ // Suffix range: last N bytes.
584
+ const suffix = parseInt(m[2], 10);
585
+ if (suffix <= 0)
586
+ return null;
587
+ start = Math.max(0, size - suffix);
588
+ end = size - 1;
589
+ }
590
+ else {
591
+ return null;
592
+ }
593
+ end = Math.min(end, size - 1);
594
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start < 0)
595
+ return null;
596
+ return { start, end };
516
597
  }
517
598
  app.post('/api/session-hub/migrate', async (c) => {
518
599
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- import{r as t,j as n}from"./react-vendor-C7Sl8SE7.js";import{c as ht,I as pt,S as ke,k as Se,a as j,u as We,d as xt,g as kt,j as St,f as bt,s as vt,M as yt}from"./index-5Q-Q7ByM.js";import{l as It,n as Xe,m as Tt,a as jt,b as Rt,p as wt,u as Nt,c as Ot,T as Lt,R as Mt,U as Ve,d as Ct,e as Ge,f as Et,L as At,I as Pt}from"./index-Dw3ty4QY.js";import{M as Je,a as Ze}from"./Modal-CBMO5UcS.js";import"./router-DHISdpPk.js";import"./Select-BiSTkS_t.js";import"./DirBrowser-CBp5nyfS.js";import"./markdown-DxQYQFeH.js";import"./ExtensionsTab-w4pkrNas.js";function Ft({snapshot:f}){const[r,h]=t.useState(f.currentIndex??0),[C,R]=t.useState(""),[x,p]=t.useState(!1),[W,k]=t.useState(null);t.useEffect(()=>{h(f.currentIndex??0),R(""),k(null)},[f.promptId,f.currentIndex]);const w=f.questions||[],v=w[r]||null,X=w.length,N=!!(v?.options&&v.options.length),d=N?!!v?.allowFreeform:!0,F=o=>{o&&(h(g=>g+1),R(""))},ne=async o=>{if(!x){p(!0),k(null);try{const g=await j.interactionSelectOption(f.promptId,o);if(!g.ok){k(g.error||"Failed to submit selection.");return}F(g.advanced)}catch(g){k(g?.message||"Network error.")}finally{p(!1)}}},O=async()=>{if(x)return;const o=C.trim();if(!o&&!v?.allowEmpty){k("Please enter a response.");return}p(!0),k(null);try{const g=await j.interactionSubmitText(f.promptId,o);if(!g.ok){k(g.error||"Failed to submit answer.");return}F(g.advanced)}catch(g){k(g?.message||"Network error.")}finally{p(!1)}},m=async()=>{if(!x){p(!0),k(null);try{const o=await j.interactionSkip(f.promptId);if(!o.ok){k(o.error||"Failed to skip.");return}F(o.advanced)}catch(o){k(o?.message||"Network error.")}finally{p(!1)}}},H=async()=>{if(!x){p(!0);try{await j.interactionCancel(f.promptId)}catch{}}},V=t.useMemo(()=>{const o=[];return f.hint&&o.push(f.hint),X>1&&o.push(`Question ${r+1} of ${X}`),o.join(" · ")||void 0},[f.hint,r,X]);return n.jsxs(Je,{open:!0,onClose:H,wide:N&&(v?.options?.length||0)>3,children:[n.jsx(Ze,{title:f.title||"Pikiloom needs your input",description:V,onClose:H}),v?n.jsxs("div",{className:"space-y-4",children:[n.jsxs("div",{children:[n.jsx("div",{className:"text-xs font-medium uppercase tracking-wide text-fg-5",children:v.header||"Question"}),n.jsx("div",{className:"mt-1 whitespace-pre-wrap text-sm leading-relaxed text-fg",children:v.prompt})]}),N&&n.jsx("div",{className:"grid grid-cols-1 gap-2 sm:grid-cols-2",children:(v.options||[]).map(o=>n.jsxs("button",{type:"button",disabled:x,onClick:()=>ne(o.value||o.label),className:ht("group rounded-lg border border-edge bg-panel-alt px-3 py-2 text-left text-sm transition","hover:border-control-border-h hover:bg-control-h hover:shadow-sm","focus:outline-none focus:ring-2 focus:ring-[var(--th-glow-a)]","disabled:cursor-not-allowed disabled:opacity-50"),children:[n.jsx("div",{className:"font-medium text-fg group-hover:text-fg",children:o.label}),o.description&&n.jsx("div",{className:"mt-0.5 text-xs leading-snug text-fg-4",children:o.description})]},o.value||o.label))}),d&&n.jsx("div",{children:n.jsx(pt,{value:C,onChange:o=>R(o.target.value),onKeyDown:o=>{o.key==="Enter"&&!o.shiftKey&&!x&&(o.preventDefault(),O())},placeholder:N?"Or type a custom answer…":"Type your answer…",disabled:x,autoFocus:!N})}),W&&n.jsx("div",{className:"rounded-md border border-red-300/40 bg-red-500/10 px-3 py-2 text-xs text-red-600",children:W}),n.jsxs("div",{className:"flex items-center justify-between gap-3",children:[n.jsx("div",{className:"text-xs text-fg-5",children:x?n.jsxs("span",{className:"inline-flex items-center gap-2",children:[n.jsx(ke,{})," Submitting…"]}):n.jsxs("span",{children:["Press ",n.jsx("kbd",{className:"rounded border border-edge bg-panel-alt px-1.5 py-0.5 text-[10px] uppercase",children:"Enter"})," to send"]})}),n.jsxs("div",{className:"flex items-center gap-2",children:[n.jsx(Se,{variant:"ghost",size:"sm",onClick:m,disabled:x,children:"Skip"}),d&&n.jsx(Se,{variant:"primary",size:"sm",onClick:O,disabled:x||!C.trim()&&!v.allowEmpty,children:"Submit"})]})]})]}):n.jsxs("div",{className:"py-6 text-center text-sm text-fg-5",children:[n.jsx(ke,{className:"mr-2 inline-block"})," Waiting for the agent…"]})]})}const Pe=12,Ye=160,Ht=96,qt=20,z=new Map;function Dt(f,r){return`${f}:${r}`}function Ut(f,r){for(z.delete(f),z.set(f,r);z.size>qt;)z.delete(z.keys().next().value)}const Gt=t.memo(function({session:r,workdir:h,active:C=!0,onSessionChange:R,initialPendingPrompt:x,initialPendingImageUrls:p,onPendingPromptConsumed:W}){const k=We(e=>e.locale),w=We(e=>e.agentStatus?.agents?.find(s=>s.agent===r.agent)??null),v=w?.selectedEffort??null,X=w?.selectedModel??null,N=w?.byokProviderName??null,d=t.useMemo(()=>xt(k),[k]),F=kt(r.agent||""),ne=St(r),O=!!x||!!(p&&p.length),[m,H]=t.useState(null),[V,o]=t.useState(!O),[g,Fe]=t.useState(!1),[i,q]=t.useState(null),[G,Y]=t.useState(!1),[se,be]=t.useState(null),[et,He]=t.useState(0),[tt,ve]=t.useState(null),[le,ae]=t.useState([]),[rt,oe]=t.useState([]),[J,ye]=t.useState([]),[S,ue]=t.useState(x||null),[L,ce]=t.useState(p||[]),[nt,ie]=t.useState(null),Ie=t.useRef(null);Ie.current=nt;const[qe,D]=t.useState([]),De=t.useRef([]);De.current=qe;const Z=t.useRef(null),[st,Ue]=t.useState(null),[de,ee]=t.useState(null),[te,Te]=t.useState(""),[E,je]=t.useState(!1),lt=!!w?.capabilities?.fork,Re=t.useRef(null),U=t.useRef(p||[]),M=t.useRef(i),$e=t.useRef(G);M.current=i,$e.current=G;const A=t.useRef(null),fe=t.useRef(null),$=t.useRef(!0),B=t.useRef(!1),me=t.useRef(null),we=t.useRef(!1),Q=t.useRef(O),re=t.useRef(!1),P=t.useRef(!1),Ne=t.useRef(!1),Oe=t.useRef(!1),Be=t.useRef({model:null,effort:null}),at=t.useCallback(e=>{Be.current=e},[]);t.useEffect(()=>{Ne.current||!O||(Ne.current=!0,x&&!S&&ue(x),p&&p.length&&!L.length&&(ce(p),U.current=p),o(!1),He(e=>e+1),W?.())},[O,W]);const I=t.useCallback(()=>{ue(null),ce(e=>{for(const s of e)URL.revokeObjectURL(s);return[]}),U.current=[],ie(null)},[]),_=t.useCallback(()=>{D(e=>{if(!e.length)return e;for(const s of e)for(const l of s.imageUrls)URL.revokeObjectURL(l);return[]}),Z.current=null},[]),Qe=t.useCallback((e,s)=>{const l=!!M.current||$e.current,a=s||[];if(l){const u=`local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`;Z.current=u,D(c=>[...c,{localId:u,taskId:null,prompt:e||"",imageUrls:a}]);return}for(const u of U.current)URL.revokeObjectURL(u);Z.current=null,ue(e||null),ce(a),U.current=a,ie(null)},[]),ot=t.useCallback(e=>{const s=Z.current;if(s){Z.current=null,D(l=>{const a=l.findIndex(c=>c.localId===s);if(a<0)return l;const u=l.slice();return u[a]={...u[a],taskId:e},u});return}ie(e)},[]),ut=t.useCallback(async()=>{if(!de)return;const e=te.trim();if(e){je(!0);try{const s=await j.forkSession(h,r.agent||"",r.sessionId,de.atTurn,e,{});if(!s.ok||!s.sessionKey){je(!1);return}const[l,a]=s.sessionKey.split(":");ee(null),Te(""),R?.({agent:l,sessionId:a,workdir:h})}finally{je(!1)}}},[de,te,h,r.agent,r.sessionId,R]);Re.current=ut;const ge=t.useCallback(async(e,s={})=>{try{const l=await It({workdir:h,agent:r.agent||"",sessionId:r.sessionId,rich:!0,turnOffset:e.turnOffset,turnLimit:e.turnLimit,lastNTurns:e.lastNTurns},{force:s.force});return l.ok?Xe(l):null}catch{return null}},[h,r.agent,r.sessionId]),T=t.useCallback(async({keepOlder:e,force:s=!1,scrollToBottom:l=!1})=>{const a=r.sessionId;if(me.current===a)return!1;me.current=a;try{const u=await ge({turnOffset:0,turnLimit:Pe},{force:s});if(!u||r.sessionId!==a)return!1;if(l&&(B.current=!0),H(c=>!c||!e?u:Tt(c,u)),o(!1),re.current&&(re.current=!1,I()),P.current){const c=P.current;P.current=!1;const b=c!==!0?c.taskId:null;M.current&&(c===!0||M.current.taskId===b)&&q(null)}return!0}finally{me.current===a&&(me.current=null)}},[ge,I,r.sessionId]),he=t.useCallback(async()=>{if(!m?.hasOlder||we.current)return;const e=A.current;e&&(fe.current={scrollHeight:e.scrollHeight,scrollTop:e.scrollTop}),we.current=!0,Fe(!0);try{const s=await ge({turnOffset:Math.max(0,m.totalTurns-m.startTurn),turnLimit:Pe});s?H(l=>l?jt(l,s):s):fe.current=null}finally{we.current=!1,Fe(!1)}},[ge,m]),pe=t.useRef(null),K=t.useCallback(e=>{if(e?.sessionId&&e.sessionId!==r.sessionId&&(Oe.current=!0,Le.current=`${r.agent}:${e.sessionId}`,R?.({agent:r.agent||"",sessionId:e.sessionId,workdir:h})),!e){const l=pe.current;Y(!1),l==="streaming"?(re.current=!0,P.current=!0,T({keepOlder:!0,force:!0,scrollToBottom:$.current})):q(null),l==="done"?(I(),_()):l===null&&Q.current&&T({keepOlder:!0,force:!0}),l!==null&&(Q.current=!1),ve(null),be(null),ae([]),oe([]),ye([]),pe.current=null;return}if(be(e.phase),ve(e.taskId||null),ae(e.queuedTaskIds&&e.queuedTaskIds.length?e.queuedTaskIds:[]),oe(e.queuedTasks&&e.queuedTasks.length?e.queuedTasks:[]),ye(Array.isArray(e.interactions)&&e.interactions.length?e.interactions:[]),e.phase==="streaming"){if(P.current&&M.current&&M.current.taskId!==null&&M.current.taskId!==(e.taskId||null)&&!(e.text||"").trim()||q({taskId:e.taskId||null,phase:"streaming",text:e.text||"",thinking:e.thinking||"",activity:e.activity,plan:e.plan??null,model:e.model??null,effort:e.effort??null,previewMeta:e.previewMeta??null,subAgents:e.previewMeta?.subAgents??null,generatingImages:e.previewMeta?.generatingImages??0,startedAt:typeof e.startedAt=="number"?e.startedAt:null,error:null,question:e.question??null}),Y(!0),e.taskId&&e.taskId!==Ie.current){const a=De.current,u=a.findIndex(c=>c.taskId===e.taskId);if(u>=0){const c=a[u];for(const b of U.current)URL.revokeObjectURL(b);ue(c.prompt||null),ce(c.imageUrls),U.current=c.imageUrls,ie(e.taskId),D(b=>b.filter((mt,gt)=>gt!==u))}}$.current&&(B.current=!0)}else if(e.phase==="queued")q(null),Y(!1);else if(e.phase==="done"){Y(!1),q(b=>b?{...b,phase:"done",error:e.error??null}:e.error?{taskId:e.taskId||null,phase:"done",text:"",thinking:"",activity:"",plan:null,model:e.model??null,effort:e.effort??null,previewMeta:e.previewMeta??null,subAgents:e.previewMeta?.subAgents??null,generatingImages:e.previewMeta?.generatingImages??0,error:e.error}:b);const l=!!e.queuedTaskIds?.length,a=M.current,u=!!a&&Rt(a),c=!!e.incomplete&&u&&!l;pe.current!=="done"&&(l||(re.current=!0),P.current=c?!1:{taskId:e.taskId||null},T({keepOlder:!0,force:!0,scrollToBottom:$.current})),l||(Q.current=!1)}const s=new Set;if(e.taskId&&s.add(e.taskId),Array.isArray(e.queuedTaskIds))for(const l of e.queuedTaskIds)s.add(l);D(l=>{let a=!1;const u=[];for(const c of l)if(!c.taskId||s.has(c.taskId))u.push(c);else{for(const b of c.imageUrls)URL.revokeObjectURL(b);a=!0}return a?u:l}),pe.current=e.phase},[I,_,T,r.sessionId,r.agent,R,h]),_e=t.useCallback(()=>{Q.current=!0,He(e=>e+1)},[]),ct=t.useCallback(async e=>{try{await j.recallSessionMessage(e),Ie.current===e&&I(),D(s=>{let l=!1;const a=[];for(const u of s)if(u.taskId===e){for(const c of u.imageUrls)URL.revokeObjectURL(c);l=!0}else a.push(u);return l?a:s}),ae(s=>s.filter(l=>l!==e)),oe(s=>s.filter(l=>l.taskId!==e)),ve(s=>s===e?null:s)}catch{}},[I]),it=t.useCallback(async e=>{try{await j.steerSession(e)}catch{}},[]),dt=t.useCallback(async()=>{try{await j.stopSession(r.agent||"",r.sessionId)}catch{}},[r.agent,r.sessionId]),xe=Dt(r.agent||"",r.sessionId);t.useEffect(()=>{if(Oe.current){Oe.current=!1;let u=!1;return T({keepOlder:!0,force:!0}).finally(()=>{u||o(!1)}),()=>{u=!0}}let e=!1;const s=wt({workdir:h,agent:r.agent||"",sessionId:r.sessionId,rich:!0,turnOffset:0,turnLimit:Pe},{allowStale:!0}),l=O&&!Ne.current,a=s?.ok?Xe(s):z.get(xe)||null;return o(l?!1:!a),H(a),q(null),Y(!1),be(null),ae([]),oe([]),ye([]),l||(I(),_(),Q.current=!1,re.current=!1,P.current=!1),$.current=!0,B.current=!0,l||T({keepOlder:!1,force:!0}).finally(()=>{e||o(!1)}),()=>{e=!0}},[T,r.agent,r.sessionId,h,xe,I,_]),t.useEffect(()=>{m&&m.turns.length>0&&Ut(xe,m)},[xe,m]),t.useEffect(()=>{C&&T({keepOlder:!0,force:!0})},[C,T]);const Le=t.useRef(`${r.agent}:${r.sessionId}`);Le.current=`${r.agent}:${r.sessionId}`,Nt("stream-update",t.useCallback(e=>{e.key===Le.current&&K(e.snapshot??null)},[K])),t.useEffect(()=>{let e=!0;return j.getSessionStreamState(r.agent||"",r.sessionId).then(s=>{e&&K(s.state)}).catch(()=>{}),()=>{e=!1}},[K,r.agent,r.sessionId,et]),Ot(t.useCallback(()=>{j.getSessionStreamState(r.agent||"",r.sessionId).then(e=>{K(e.state)}).catch(()=>{}),T({keepOlder:!0,force:!0})},[K,r.agent,r.sessionId,T])),t.useEffect(()=>{!Q.current&&ne!=="running"&&!G&&!i&&!se&&le.length===0&&(I(),_())},[ne,G,i,se,le.length,I,_]),t.useLayoutEffect(()=>{const e=fe.current,s=A.current;!e||!s||(fe.current=null,s.scrollTop=e.scrollTop+(s.scrollHeight-e.scrollHeight))},[m?.turns.length]),t.useLayoutEffect(()=>{if(!B.current)return;const e=A.current;e&&(B.current=!1,e.scrollTop=e.scrollHeight,requestAnimationFrame(()=>{$.current&&(e.scrollTop=e.scrollHeight)}))},[m,i]),t.useLayoutEffect(()=>{if(!S)return;const e=A.current;e&&(e.scrollTop=e.scrollHeight)},[S]),t.useEffect(()=>{if(!m?.hasOlder||V||g)return;const e=A.current;e&&e.scrollHeight<=e.clientHeight+Ye&&he()},[m?.hasOlder,m?.turns.length,he,V,g]);const ft=t.useCallback(()=>{const e=A.current;if(!e)return;const s=e.scrollHeight-e.scrollTop-e.clientHeight;$.current=s<=Ht,e.scrollTop<=Ye&&he()},[he]),Me=i?.model||r.model||X||null,Ce=bt(r.agent||"",i?.effort||r.thinkingEffort||v||null,r.workflowEnabled??w?.workflowEnabled)||null,Ke=Me?vt(Me):null,Ee=yt(r,{streaming:G,hasLiveStream:!!i,streamPhase:se,queuedTaskCount:le.length}),y=m?.turns||[],Ae=t.useMemo(()=>{if(!L.length||!y.length)return!1;const e=y[y.length-1];return!e.user||(e.user.text?.trim()||"")!==(S||"").trim()?!1:e.user.blocks.filter(l=>l.type==="image").length<L.length},[y,S,L.length]),ze=t.useMemo(()=>{let e=y;if(Ae){const b=e[e.length-1];e=[...e.slice(0,-1),{...b,user:null}]}if(!i||!e.length)return e;const s=e[e.length-1];if(!s.assistant)return e;const l=S??(i.question||null),a=(i.text||"").trim(),u=s.assistant.text?.trim()||"";return(l!=null?s.user?.text?.trim()===l.trim():!!u&&!!a&&(a.startsWith(u)||u.startsWith(a)))?[...e.slice(0,-1),{...s,assistant:null}]:e},[y,i,S,Ae]);return n.jsxs("div",{className:"flex flex-col h-full overflow-hidden",children:[n.jsx("div",{ref:A,onScroll:ft,className:"flex-1 overflow-y-auto overscroll-contain",children:V&&!S&&!L.length&&!i?n.jsx("div",{className:"flex items-center justify-center py-20",children:n.jsx(ke,{className:"h-5 w-5 text-fg-4"})}):ze.length===0&&!S&&!L.length&&!i&&!Ee?n.jsx("div",{className:"py-20 text-center text-[13px] text-fg-5",children:d("hub.noMessages")}):n.jsxs("div",{className:"max-w-[900px] mx-auto px-6 py-6 space-y-0",children:[(m?.hasOlder||g)&&n.jsxs("div",{className:"mb-4 flex items-center justify-center gap-2 text-[11px] text-fg-5",children:[g?n.jsx(ke,{className:"h-3 w-3 text-fg-5"}):n.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-fg-5/35"}),n.jsx("span",{children:d(g?"hub.loadingOlderTurns":"hub.loadOlderTurnsHint")})]}),r.migratedFrom?.kind==="fork"&&r.migratedFrom.sessionId&&n.jsxs("button",{type:"button",onClick:()=>R?.({agent:r.migratedFrom.agent||r.agent||"",sessionId:r.migratedFrom.sessionId,workdir:h}),className:"mb-4 inline-flex items-center gap-1.5 rounded-md border border-edge bg-panel-alt px-2.5 py-1 text-[11px] text-fg-5 transition hover:border-edge-h hover:text-fg-2",title:`#${r.migratedFrom.sessionId.slice(0,8)}`,children:[n.jsxs("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[n.jsx("circle",{cx:"6",cy:"6",r:"2"}),n.jsx("circle",{cx:"18",cy:"6",r:"2"}),n.jsx("circle",{cx:"12",cy:"20",r:"2"}),n.jsx("path",{d:"M6 8v3a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V8"}),n.jsx("path",{d:"M12 14v4"})]}),n.jsx("span",{children:d("hub.forkBadge")}),n.jsxs("span",{className:"font-mono",children:["#",r.migratedFrom.sessionId.slice(0,8)]}),typeof r.migratedFrom.forkedAtTurn=="number"&&n.jsxs("span",{className:"text-fg-5/70",children:["· ",d("hub.forkBadgeAt").replace("{turn}",String(r.migratedFrom.forkedAtTurn+1))]})]}),ze.map((e,s)=>{const l=(m?.startTurn||0)+s;return n.jsx(Lt,{turn:e,turnIndex:l,agent:r.agent||"",meta:F,model:Ke,effort:Ce,providerName:N,t:d,workdir:h,onResend:a=>{B.current=!0,Qe(a);const u=Be.current;j.sendSessionMessage(h,r.agent||"",r.sessionId,a,{model:u.model||Me||void 0,effort:u.effort||Ce||void 0}).then(c=>{c.ok&&_e()}).catch(()=>{I()})},onEdit:a=>Ue(a),onFork:lt?a=>{Te(""),ee({atTurn:a})}:void 0},`${m?.startTurn||0}:${s}`)}),Ee&&n.jsx("div",{className:"mb-5 animate-in",children:n.jsx(Mt,{detail:Ee,t:d})}),(S||L.length>0)&&(Ae||!(S&&y.length>0&&y[y.length-1]?.user?.text?.trim()===S.trim()))&&n.jsxs("div",{className:"session-turn",children:[n.jsx(Ve,{text:S||"",blocks:L.map(e=>({type:"image",content:e})),t:d}),!i&&n.jsx("div",{className:"mt-3 mb-5 animate-in",children:n.jsx(Ct,{className:"text-fg-5"})})]}),i&&Ge(i)&&!S&&i.question&&!(y.length>0&&y[y.length-1]?.user?.text?.trim()===i.question.trim())&&n.jsx("div",{className:"session-turn",children:n.jsx(Ve,{text:i.question,t:d})}),i&&Ge(i)&&n.jsxs("div",{className:"mb-6",children:[n.jsx(Et,{agent:r.agent||"",meta:F,model:Ke,effort:Ce,providerName:N,previewMeta:i.previewMeta,liveStartedAt:i.phase==="streaming"?i.startedAt??null:null}),n.jsx(At,{stream:i,t:d,workdir:h})]}),n.jsx("div",{className:"h-4"})]})}),n.jsx(Pt,{session:r,workdir:h,onStreamQueued:_e,onSendStart:Qe,onSendTaskAssigned:ot,onSessionChange:R,t:d,streamPhase:se,streamTaskId:tt,queuedTaskIds:le,queuedTasks:rt,pendingQueuedSends:qe,onRecall:ct,onSteer:it,onStopAll:dt,editDraft:st,onEditDraftConsumed:()=>Ue(null),onSelectionChange:at}),de&&n.jsxs(Je,{open:!0,onClose:()=>{E||ee(null)},children:[n.jsx(Ze,{title:d("hub.forkPromptTitle"),description:d("hub.forkPromptHint"),onClose:()=>{E||ee(null)}}),n.jsx("textarea",{autoFocus:!0,value:te,disabled:E,onChange:e=>Te(e.target.value),onKeyDown:e=>{e.key==="Enter"&&(e.metaKey||e.ctrlKey)&&te.trim()&&!E&&(e.preventDefault(),Re.current?.())},placeholder:d("hub.forkPromptPlaceholder"),className:"w-full min-h-[120px] resize-y rounded-md border border-edge bg-panel-alt px-3 py-2 text-[13px] leading-relaxed text-fg outline-none focus:border-edge-h"}),n.jsxs("div",{className:"mt-4 flex items-center justify-end gap-2",children:[n.jsx(Se,{variant:"ghost",disabled:E,onClick:()=>ee(null),children:d("modal.cancel")}),n.jsx(Se,{variant:"primary",disabled:E||!te.trim(),onClick:()=>{Re.current?.()},children:d(E?"hub.forkSubmitting":"hub.forkSubmit")})]})]}),C&&J.length>0&&n.jsx(Ft,{snapshot:J[J.length-1]},J[J.length-1].promptId)]})});export{Gt as SessionPanel};