pikiloom 0.4.15 → 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 (41) hide show
  1. package/README.md +2 -1
  2. package/dashboard/dist/assets/{AgentTab-CKoy_-w4.js → AgentTab-CDVhy5K1.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-DpbuN0OL.js → DirBrowser-BElI1-4D.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-ymr7K8dU.js → ExtensionsTab-BB8ipJ77.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-CaTtCn3l.js → IMAccessTab-IZt_yXoG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-DA-9kJxp.js → Modal-C1EAGSL1.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-BkLIRnNK.js → Modals-DYUV5yR9.js} +1 -1
  8. package/dashboard/dist/assets/{Select-B0pZtuzF.js → Select-BnsbE6Qv.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-Ca_TVTT1.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-B9TcGMzc.js → SystemTab-Dk6k2OTt.js} +1 -1
  11. package/dashboard/dist/assets/index-CK-3CNRp.js +3 -0
  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-i_XUH0xm.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/bridge.js +201 -7
  20. package/dist/agent/mcp/extensions.js +20 -9
  21. package/dist/agent/mcp/tools/workspace.js +4 -3
  22. package/dist/agent/stream.js +3 -2
  23. package/dist/bot/bot.js +83 -4
  24. package/dist/bot/commands.js +48 -2
  25. package/dist/bot/menu.js +1 -0
  26. package/dist/bot/session-hub.js +1 -1
  27. package/dist/channels/dingtalk/bot.js +9 -1
  28. package/dist/channels/discord/bot.js +9 -1
  29. package/dist/channels/feishu/bot.js +8 -1
  30. package/dist/channels/slack/bot.js +9 -1
  31. package/dist/channels/telegram/bot.js +8 -1
  32. package/dist/channels/wecom/bot.js +9 -1
  33. package/dist/channels/weixin/bot.js +9 -1
  34. package/dist/cli/main.js +1 -0
  35. package/dist/dashboard/routes/config.js +134 -12
  36. package/dist/dashboard/routes/sessions.js +108 -27
  37. package/package.json +1 -1
  38. package/dashboard/dist/assets/SessionPanel-CYQtZZNX.js +0 -1
  39. package/dashboard/dist/assets/index-BCYshErN.js +0 -3
  40. package/dashboard/dist/assets/index-C5irxzzD.js +0 -23
  41. package/dashboard/dist/assets/index-FD86DEDF.css +0 -1
@@ -6,8 +6,9 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import os from 'node:os';
8
8
  import { spawn, spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
9
10
  import { loadUserConfig, saveUserConfig, applyUserConfig, hasUserConfigFile } from '../../core/config/user-config.js';
10
- import { expandTilde } from '../../core/platform.js';
11
+ import { expandTilde, whichSync } from '../../core/platform.js';
11
12
  import { readGitStatus } from '../../core/git.js';
12
13
  import { isSetupReady } from '../../cli/onboarding.js';
13
14
  import { validateDingtalkConfig, validateDiscordConfig, validateFeishuConfig, validateSlackConfig, validateTelegramConfig, validateWecomConfig, validateWeixinConfig, } from '../../core/config/validation.js';
@@ -66,7 +67,96 @@ function runOpenCommand(command, args) {
66
67
  throw new Error(detail || `Failed to run ${command} ${args.join(' ')}`);
67
68
  }
68
69
  }
69
- function openPathWithTarget(filePath, target, isDirectory) {
70
+ function stripOpenPathWrapping(value) {
71
+ let text = value.trim();
72
+ const pairs = [['`', '`'], ['"', '"'], ["'", "'"], ['<', '>']];
73
+ let changed = true;
74
+ while (changed && text.length >= 2) {
75
+ changed = false;
76
+ for (const [left, right] of pairs) {
77
+ if (text.startsWith(left) && text.endsWith(right)) {
78
+ text = text.slice(left.length, -right.length).trim();
79
+ changed = true;
80
+ }
81
+ }
82
+ }
83
+ return text;
84
+ }
85
+ function decodeOpenPathInput(raw) {
86
+ const text = stripOpenPathWrapping(raw);
87
+ if (text.startsWith('file://')) {
88
+ try {
89
+ return fileURLToPath(text);
90
+ }
91
+ catch {
92
+ return decodeURI(text.slice('file://'.length));
93
+ }
94
+ }
95
+ if (text.startsWith('vscode://file/')) {
96
+ return decodeURI(`/${text.slice('vscode://file/'.length)}`);
97
+ }
98
+ return text;
99
+ }
100
+ function resolveOpenBasePath(basePath) {
101
+ const base = typeof basePath === 'string' && basePath.trim()
102
+ ? basePath.trim()
103
+ : runtime.getRuntimeWorkdir(loadUserConfig());
104
+ return path.resolve(expandTilde(base || process.cwd()));
105
+ }
106
+ function splitExistingLineSuffix(candidate) {
107
+ const normalized = path.normalize(candidate);
108
+ if (fs.existsSync(normalized))
109
+ return { filePath: normalized, line: null, column: null };
110
+ const match = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(normalized);
111
+ if (!match || !match[1])
112
+ return { filePath: normalized, line: null, column: null };
113
+ const filePath = path.normalize(match[1]);
114
+ if (!fs.existsSync(filePath))
115
+ return { filePath: normalized, line: null, column: null };
116
+ return {
117
+ filePath,
118
+ line: Number(match[2]),
119
+ column: match[3] ? Number(match[3]) : null,
120
+ };
121
+ }
122
+ export function resolveOpenPathLocator(rawPath, basePath) {
123
+ const decoded = decodeOpenPathInput(rawPath);
124
+ const expanded = expandTilde(decoded);
125
+ const absolute = path.isAbsolute(expanded)
126
+ ? path.resolve(expanded)
127
+ : path.resolve(resolveOpenBasePath(basePath), expanded);
128
+ return splitExistingLineSuffix(absolute);
129
+ }
130
+ function editorGotoArg(filePath, location) {
131
+ if (!location?.line)
132
+ return null;
133
+ return `${filePath}:${location.line}${location.column ? `:${location.column}` : ''}`;
134
+ }
135
+ function tryOpenCommand(command, args) {
136
+ if (!whichSync(command))
137
+ return false;
138
+ try {
139
+ runOpenCommand(command, args);
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ function tryOpenVSCodeUrl(filePath, location) {
147
+ if (!location?.line)
148
+ return false;
149
+ const suffix = `:${location.line}${location.column ? `:${location.column}` : ''}`;
150
+ try {
151
+ runOpenCommand('open', [`vscode://file${encodeURI(filePath)}${suffix}`]);
152
+ return true;
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ function openPathWithTarget(filePath, target, isDirectory, location) {
159
+ const gotoArg = isDirectory ? null : editorGotoArg(filePath, location);
70
160
  if (process.platform === 'darwin') {
71
161
  switch (target) {
72
162
  case 'finder':
@@ -76,13 +166,21 @@ function openPathWithTarget(filePath, target, isDirectory) {
76
166
  runOpenCommand('open', [filePath]);
77
167
  return;
78
168
  case 'cursor':
169
+ if (gotoArg && tryOpenCommand('cursor', ['-g', gotoArg]))
170
+ return;
79
171
  runOpenCommand('open', ['-a', 'Cursor', filePath]);
80
172
  return;
81
173
  case 'windsurf':
174
+ if (gotoArg && tryOpenCommand('windsurf', ['-g', gotoArg]))
175
+ return;
82
176
  runOpenCommand('open', ['-a', 'Windsurf', filePath]);
83
177
  return;
84
178
  case 'vscode':
85
179
  default:
180
+ if (gotoArg && tryOpenCommand('code', ['-g', gotoArg]))
181
+ return;
182
+ if (gotoArg && tryOpenVSCodeUrl(filePath, location))
183
+ return;
86
184
  runOpenCommand('open', ['-a', 'Visual Studio Code', filePath]);
87
185
  return;
88
186
  }
@@ -90,10 +188,16 @@ function openPathWithTarget(filePath, target, isDirectory) {
90
188
  if (process.platform === 'win32') {
91
189
  switch (target) {
92
190
  case 'cursor':
93
- runOpenCommand('cursor', [filePath]);
191
+ if (gotoArg)
192
+ runOpenCommand('cursor', ['-g', gotoArg]);
193
+ else
194
+ runOpenCommand('cursor', [filePath]);
94
195
  return;
95
196
  case 'windsurf':
96
- runOpenCommand('windsurf', [filePath]);
197
+ if (gotoArg)
198
+ runOpenCommand('windsurf', ['-g', gotoArg]);
199
+ else
200
+ runOpenCommand('windsurf', [filePath]);
97
201
  return;
98
202
  case 'finder':
99
203
  case 'default':
@@ -101,16 +205,25 @@ function openPathWithTarget(filePath, target, isDirectory) {
101
205
  return;
102
206
  case 'vscode':
103
207
  default:
104
- runOpenCommand('code', [filePath]);
208
+ if (gotoArg)
209
+ runOpenCommand('code', ['-g', gotoArg]);
210
+ else
211
+ runOpenCommand('code', [filePath]);
105
212
  return;
106
213
  }
107
214
  }
108
215
  switch (target) {
109
216
  case 'cursor':
110
- runOpenCommand('cursor', [filePath]);
217
+ if (gotoArg)
218
+ runOpenCommand('cursor', ['-g', gotoArg]);
219
+ else
220
+ runOpenCommand('cursor', [filePath]);
111
221
  return;
112
222
  case 'windsurf':
113
- runOpenCommand('windsurf', [filePath]);
223
+ if (gotoArg)
224
+ runOpenCommand('windsurf', ['-g', gotoArg]);
225
+ else
226
+ runOpenCommand('windsurf', [filePath]);
114
227
  return;
115
228
  case 'finder':
116
229
  case 'default':
@@ -118,7 +231,10 @@ function openPathWithTarget(filePath, target, isDirectory) {
118
231
  return;
119
232
  case 'vscode':
120
233
  default:
121
- runOpenCommand('code', [filePath]);
234
+ if (gotoArg)
235
+ runOpenCommand('code', ['-g', gotoArg]);
236
+ else
237
+ runOpenCommand('code', [filePath]);
122
238
  return;
123
239
  }
124
240
  }
@@ -453,14 +569,20 @@ app.post('/api/open-in-editor', async (c) => {
453
569
  try {
454
570
  const body = await c.req.json();
455
571
  const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
572
+ const basePath = typeof body?.basePath === 'string' && body.basePath.trim()
573
+ ? body.basePath.trim()
574
+ : typeof body?.workdir === 'string' && body.workdir.trim()
575
+ ? body.workdir.trim()
576
+ : null;
456
577
  const target = isOpenTarget(body?.target) ? body.target : 'vscode';
457
578
  if (!filePath)
458
579
  return c.json({ ok: false, error: 'filePath is required' }, 400);
459
- if (!fs.existsSync(filePath))
580
+ const resolved = resolveOpenPathLocator(filePath, basePath);
581
+ if (!fs.existsSync(resolved.filePath))
460
582
  return c.json({ ok: false, error: 'Path not found' }, 404);
461
- const stat = fs.statSync(filePath);
462
- openPathWithTarget(filePath, target, stat.isDirectory());
463
- return c.json({ ok: true });
583
+ const stat = fs.statSync(resolved.filePath);
584
+ openPathWithTarget(resolved.filePath, target, stat.isDirectory(), resolved);
585
+ return c.json({ ok: true, filePath: resolved.filePath, line: resolved.line, column: resolved.column });
464
586
  }
465
587
  catch (err) {
466
588
  const detail = err instanceof Error ? err.message : String(err);
@@ -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.15",
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-BCYshErN.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-C5irxzzD.js";import{M as Je,a as Ze}from"./Modal-DA-9kJxp.js";import"./router-DHISdpPk.js";import"./Select-B0pZtuzF.js";import"./DirBrowser-DpbuN0OL.js";import"./markdown-DxQYQFeH.js";import"./ExtensionsTab-ymr7K8dU.js";function Ft({snapshot:f}){const[r,p]=t.useState(f.currentIndex??0),[C,R]=t.useState(""),[x,h]=t.useState(!1),[W,k]=t.useState(null);t.useEffect(()=>{p(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&&(p(g=>g+1),R(""))},ne=async o=>{if(!x){h(!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{h(!1)}}},O=async()=>{if(x)return;const o=C.trim();if(!o&&!v?.allowEmpty){k("Please enter a response.");return}h(!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{h(!1)}},m=async()=>{if(!x){h(!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{h(!1)}}},H=async()=>{if(!x){h(!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:p,active:C=!0,onSessionChange:R,initialPendingPrompt:x,initialPendingImageUrls:h,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||!!(h&&h.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(h||[]),[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(h||[]),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),h&&h.length&&!L.length&&(ce(h),U.current=h),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(p,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:p})}finally{je(!1)}}},[de,te,p,r.agent,r.sessionId,R]);Re.current=ut;const ge=t.useCallback(async(e,s={})=>{try{const l=await It({workdir:p,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}},[p,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:p})),!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,p]),_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:p,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,p,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:p}),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,onResend:a=>{B.current=!0,Qe(a);const u=Be.current;j.sendSessionMessage(p,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})]}),n.jsx("div",{className:"h-4"})]})}),n.jsx(Pt,{session:r,workdir:p,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};