pikiloom 0.4.42 → 0.4.43

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 (25) hide show
  1. package/dashboard/dist/assets/{AgentTab-B9z4W2mh.js → AgentTab-B8V2eV_D.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-2bL7sFKk.js → ConnectionModal-BOVwXLJB.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-CA4k8E_A.js → DirBrowser-DbeAWYiL.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-CRjTmFRl.js → ExtensionsTab-BMS3PW9N.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-DDb7rsHe.js → IMAccessTab-B1sRgJk3.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-DDiBG4kb.js → Modal-DCwGz46r.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-CsUQjpxn.js → Modals-Bh_0en5P.js} +1 -1
  8. package/dashboard/dist/assets/{Select-CG_7h_Tz.js → Select-DC6zguGC.js} +1 -1
  9. package/dashboard/dist/assets/{SessionPanel-B5964GUj.js → SessionPanel-DNlLd-PL.js} +1 -1
  10. package/dashboard/dist/assets/{SystemTab-D7r1PsC9.js → SystemTab-uFqZinIZ.js} +1 -1
  11. package/dashboard/dist/assets/{index-BbplxgnQ.js → index-CL5H13Cl.js} +2 -2
  12. package/dashboard/dist/assets/index-DooLxbPX.js +23 -0
  13. package/dashboard/dist/assets/{shared-Bv8WvQSo.js → shared-Byy2BNLq.js} +1 -1
  14. package/dashboard/dist/index.html +1 -1
  15. package/dist/agent/accounts.js +2 -2
  16. package/dist/agent/drivers/claude.js +84 -38
  17. package/dist/agent/drivers/codex.js +30 -2
  18. package/dist/agent/kernel-bridge.js +18 -2
  19. package/dist/dashboard/routes/accounts.js +7 -0
  20. package/dist/dashboard/routes/agents.js +1 -1
  21. package/package.json +1 -1
  22. package/packages/kernel/dist/drivers/claude.d.ts +3 -1
  23. package/packages/kernel/dist/drivers/claude.js +126 -9
  24. package/packages/kernel/dist/drivers/codex.js +15 -3
  25. package/dashboard/dist/assets/index-CULTTvtY.js +0 -23
@@ -109,6 +109,22 @@ function buildKernelDriver(kernel, opts) {
109
109
  };
110
110
  }
111
111
  }
112
+ // Kernel plan steps key their text as { text }; pikiloom's StreamPlan and the entire dashboard
113
+ // pipeline (pikichannel adapter -> ws.ts -> PlanProgressCard) key it as { step }. Translate at
114
+ // this seam — the same place we already remap usage fields — so codex/claude task lists render
115
+ // their text. Without it the progress count ("0/4") shows but every row is blank (the card reads
116
+ // step.step, which is undefined on the kernel shape).
117
+ export function toPikiloomPlan(plan) {
118
+ if (!plan || !Array.isArray(plan.steps))
119
+ return null;
120
+ const steps = plan.steps
121
+ .map((st) => ({
122
+ step: typeof st?.text === 'string' ? st.text : typeof st?.step === 'string' ? st.step : '',
123
+ status: (st?.status === 'completed' ? 'completed' : st?.status === 'inProgress' ? 'inProgress' : 'pending'),
124
+ }))
125
+ .filter((st) => st.step.trim());
126
+ return steps.length ? { explanation: typeof plan.explanation === 'string' ? plan.explanation : null, steps } : null;
127
+ }
112
128
  let _kernel = null;
113
129
  export async function loadKernel() {
114
130
  if (_kernel)
@@ -185,7 +201,7 @@ export async function kernelStream(opts) {
185
201
  providerName: opts.byokProviderName ?? null,
186
202
  };
187
203
  try {
188
- opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, s.plan ?? null);
204
+ opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, toPikiloomPlan(s.plan));
189
205
  }
190
206
  catch { /* isolate */ }
191
207
  };
@@ -229,7 +245,7 @@ export async function kernelStream(opts) {
229
245
  ok: !!result.ok,
230
246
  message: (result.text || snapshot.text || '').trim() || (finalError ?? '(no output)'),
231
247
  thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
232
- plan: snapshot.plan ?? null,
248
+ plan: toPikiloomPlan(snapshot.plan),
233
249
  sessionId: finalSessionId,
234
250
  workspacePath: null,
235
251
  model: input.model ?? null,
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { allDriverIds } from '../../agent/index.js';
3
3
  import { accountAgentSupported, listAccounts, getAccount, addAccount, updateAccount, removeAccount, getActiveAccountId, setActiveAccount, probeAccountUsage, MAX_ACCOUNTS_PER_AGENT, } from '../../agent/accounts.js';
4
+ import { invalidateAgentStatus } from './agents.js';
4
5
  const app = new Hono();
5
6
  function agentError(agent) {
6
7
  if (!allDriverIds().includes(agent))
@@ -104,6 +105,12 @@ app.post('/api/agents/:agent/active-account', async (c) => {
104
105
  return c.json({ ok: false, error: 'accountId (string|null) is required' }, 400);
105
106
  try {
106
107
  setActiveAccount(agent, accountId);
108
+ // The identity that drives usage just changed: drop the cached agent-status (its native /
109
+ // default-login usage is now stale) and force-refresh the newly-active account's own usage,
110
+ // so the header reflects the latest numbers immediately instead of the previous account's.
111
+ if (accountId)
112
+ await probeAccountUsage(agent, accountId, { force: true });
113
+ await invalidateAgentStatus();
107
114
  return c.json({ ok: true, agent, activeAccountId: accountId });
108
115
  }
109
116
  catch (e) {
@@ -238,7 +238,7 @@ function getCachedAgentStatus() {
238
238
  }
239
239
  return refreshStatusCache();
240
240
  }
241
- function invalidateAgentStatus(config, opts) {
241
+ export function invalidateAgentStatus(config, opts) {
242
242
  statusCache.pending = null;
243
243
  return refreshStatusCache(config, opts);
244
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.42",
3
+ "version": "0.4.43",
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": {
@@ -30,9 +30,11 @@ export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
30
30
  export declare function claudeContextWindowFromModel(model: unknown): number | null;
31
31
  export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
32
32
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
33
- export declare function claudeUserMessage(text: string): string;
33
+ export declare function claudeUserMessage(text: string, attachments?: string[]): string;
34
34
  export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
35
35
  export declare function todoWriteToPlan(input: any): UniversalPlan | null;
36
+ export declare function readClaudeTaskCreateId(ev: any, block: any): string | null;
37
+ export declare function rebuildClaudeTaskPlan(s: any): UniversalPlan | null;
36
38
  export declare function shortToolValue(value: unknown, max?: number): string;
37
39
  export declare function summarizeToolUse(name: string, input: any): string;
38
40
  export declare function firstResultLine(content: any): string;
@@ -1,4 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { extname } from 'node:path';
2
4
  import { discoverClaudeNativeSessions } from '../workspace/native.js';
3
5
  // Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
4
6
  // events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
@@ -28,6 +30,9 @@ export class ClaudeDriver {
28
30
  }
29
31
  run(input, ctx) {
30
32
  const steerable = !!input.steerable;
33
+ // Image attachments must ride a stream-json user message (text-only stdin can't carry images),
34
+ // so enable that input mode whenever there are attachments, not just for mid-turn steering.
35
+ const useStreamJson = steerable || !!input.attachments?.length;
31
36
  const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
32
37
  if (input.model)
33
38
  args.push('--model', input.model);
@@ -41,8 +46,10 @@ export class ClaudeDriver {
41
46
  args.push('--mcp-config', input.mcpConfigPath);
42
47
  if (input.permissionMode)
43
48
  args.push('--permission-mode', input.permissionMode); // parity: keep bypass/accept-edits on the kernel path
49
+ if (useStreamJson)
50
+ args.push('--input-format', 'stream-json');
44
51
  if (steerable)
45
- args.push('--input-format', 'stream-json', '--replay-user-messages'); // parity: mid-turn steer
52
+ args.push('--replay-user-messages'); // parity: mid-turn steer
46
53
  if (input.extraArgs?.length)
47
54
  args.push(...input.extraArgs);
48
55
  const state = {
@@ -54,6 +61,9 @@ export class ClaudeDriver {
54
61
  contextWindow: null, turnOutputTokensBase: 0,
55
62
  subAgents: new Map(),
56
63
  tools: new Map(),
64
+ taskList: new Map(),
65
+ taskOrder: [],
66
+ pendingTaskCreates: new Map(),
57
67
  };
58
68
  return new Promise((resolve) => {
59
69
  let child;
@@ -90,9 +100,9 @@ export class ClaudeDriver {
90
100
  else
91
101
  ctx.signal.addEventListener('abort', onAbort, { once: true });
92
102
  if (steerable) {
93
- ctx.registerSteer(async (prompt) => {
103
+ ctx.registerSteer(async (prompt, attachments) => {
94
104
  try {
95
- child.stdin.write(claudeUserMessage(prompt) + '\n');
105
+ child.stdin.write(claudeUserMessage(prompt, attachments) + '\n');
96
106
  return true;
97
107
  }
98
108
  catch {
@@ -136,8 +146,11 @@ export class ClaudeDriver {
136
146
  finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
137
147
  });
138
148
  try {
139
- if (steerable)
140
- child.stdin.write(claudeUserMessage(input.prompt) + '\n'); // keep stdin open for steering
149
+ if (useStreamJson) {
150
+ child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
151
+ if (!steerable)
152
+ child.stdin.end(); // single turn; steer mode keeps stdin open for replay
153
+ }
141
154
  else {
142
155
  child.stdin.write(input.prompt);
143
156
  child.stdin.end();
@@ -301,6 +314,34 @@ export function handleClaudeEvent(ev, s, emit) {
301
314
  emit({ type: 'plan', plan });
302
315
  continue;
303
316
  }
317
+ // Task list (current Claude mechanism): stash the subject; the tool_result assigns the id.
318
+ // Plan-only — like the legacy driver these never surface as Activity rows.
319
+ if (name === 'TaskCreate') {
320
+ const subject = typeof b.input?.subject === 'string' ? b.input.subject.trim() : '';
321
+ if (subject)
322
+ (s.pendingTaskCreates ||= new Map()).set(id, { subject });
323
+ continue;
324
+ }
325
+ if (name === 'TaskUpdate') {
326
+ const taskId = String(b.input?.taskId ?? '').trim();
327
+ const rawStatus = String(b.input?.status ?? '').trim().toLowerCase();
328
+ if (taskId) {
329
+ if (rawStatus === 'deleted') {
330
+ s.taskList?.delete(taskId);
331
+ if (Array.isArray(s.taskOrder))
332
+ s.taskOrder = s.taskOrder.filter((x) => x !== taskId);
333
+ }
334
+ else if (rawStatus) {
335
+ const existing = s.taskList?.get(taskId);
336
+ if (existing)
337
+ existing.status = rawStatus;
338
+ }
339
+ const plan = rebuildClaudeTaskPlan(s);
340
+ if (plan)
341
+ emit({ type: 'plan', plan });
342
+ }
343
+ continue;
344
+ }
304
345
  if (name === 'Task' || name === 'Agent') {
305
346
  const input = b.input || {};
306
347
  const sub = {
@@ -348,6 +389,23 @@ export function handleClaudeEvent(ev, s, emit) {
348
389
  continue;
349
390
  emitClaudeImages(b.content || [], s, emit);
350
391
  const id = String(b.tool_use_id || '').trim();
392
+ // TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
393
+ if (id && s.pendingTaskCreates?.has(id)) {
394
+ const pending = s.pendingTaskCreates.get(id);
395
+ const assignedId = readClaudeTaskCreateId(ev, b);
396
+ if (pending && assignedId) {
397
+ s.pendingTaskCreates.delete(id);
398
+ (s.taskList ||= new Map());
399
+ (s.taskOrder ||= []);
400
+ if (!s.taskList.has(assignedId))
401
+ s.taskOrder.push(assignedId);
402
+ s.taskList.set(assignedId, { subject: pending.subject, status: 'pending' });
403
+ const plan = rebuildClaudeTaskPlan(s);
404
+ if (plan)
405
+ emit({ type: 'plan', plan });
406
+ }
407
+ continue;
408
+ }
351
409
  const tool = id ? s.tools?.get(id) : undefined;
352
410
  if (!tool)
353
411
  continue;
@@ -384,10 +442,30 @@ export function handleClaudeEvent(ev, s, emit) {
384
442
  return;
385
443
  }
386
444
  }
387
- // A stream-json user message (for --input-format stream-json; used to send the prompt and
388
- // to inject mid-turn steer messages while stdin stays open).
389
- export function claudeUserMessage(text) {
390
- return JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text }] } });
445
+ // Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
446
+ const CLAUDE_IMAGE_MIME = {
447
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
448
+ '.gif': 'image/gif', '.webp': 'image/webp',
449
+ };
450
+ // A stream-json user message (for --input-format stream-json; used to send the prompt and to
451
+ // inject mid-turn steer messages while stdin stays open). Image attachments are inlined as base64
452
+ // image content blocks so the model actually sees them; other files become a text note. Without
453
+ // this the kernel path sent text only and silently dropped pasted/attached images.
454
+ export function claudeUserMessage(text, attachments) {
455
+ const content = [];
456
+ for (const filePath of attachments || []) {
457
+ const mime = CLAUDE_IMAGE_MIME[extname(filePath).toLowerCase()];
458
+ if (mime) {
459
+ try {
460
+ content.push({ type: 'image', source: { type: 'base64', media_type: mime, data: readFileSync(filePath).toString('base64') } });
461
+ continue;
462
+ }
463
+ catch { /* unreadable -> fall through to a text note */ }
464
+ }
465
+ content.push({ type: 'text', text: `[Attached file: ${filePath}]` });
466
+ }
467
+ content.push({ type: 'text', text });
468
+ return JSON.stringify({ type: 'user', message: { role: 'user', content } });
391
469
  }
392
470
  // Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
393
471
  export function emitClaudeImages(blocks, s, emit) {
@@ -425,6 +503,45 @@ export function todoWriteToPlan(input) {
425
503
  }
426
504
  return steps.length ? { explanation: null, steps } : null;
427
505
  }
506
+ // ── Claude task-list (TaskCreate / TaskUpdate) -> UniversalPlan ──────────────────────
507
+ // Current Claude Code drives its task list through TaskCreate/TaskUpdate, NOT TodoWrite
508
+ // (TodoWrite is the legacy mechanism). TaskCreate carries the subject; its tool_result then
509
+ // assigns a stable task id (toolUseResult.task.id, or "Task #N" in the text). TaskUpdate flips
510
+ // a task's status by id. We accumulate the list in driver state and re-emit the whole plan on
511
+ // each change. Without this the kernel path never emits a plan event for Claude, so the
512
+ // dashboard's task-list card never renders. Ported from pikiloom's legacy claude driver.
513
+ // The assigned task id from a TaskCreate tool_result. Prefer the structured field; fall back
514
+ // to parsing "Task #N" from a string result.
515
+ export function readClaudeTaskCreateId(ev, block) {
516
+ const structured = ev?.toolUseResult?.task?.id;
517
+ if (structured != null && String(structured).trim())
518
+ return String(structured).trim();
519
+ const content = block?.content;
520
+ if (typeof content === 'string') {
521
+ const m = content.match(/Task #(\d+)/);
522
+ if (m)
523
+ return m[1];
524
+ }
525
+ return null;
526
+ }
527
+ export function rebuildClaudeTaskPlan(s) {
528
+ if (!Array.isArray(s?.taskOrder) || !s.taskOrder.length)
529
+ return null;
530
+ const steps = [];
531
+ for (const id of s.taskOrder) {
532
+ const task = s.taskList?.get(id);
533
+ if (!task)
534
+ continue;
535
+ const lowered = String(task.status || '').toLowerCase();
536
+ const status = lowered === 'completed' ? 'completed'
537
+ : (lowered === 'in_progress' || lowered === 'inprogress') ? 'inProgress'
538
+ : 'pending';
539
+ const text = String(task.subject || '').trim();
540
+ if (text)
541
+ steps.push({ text, status });
542
+ }
543
+ return steps.length ? { explanation: null, steps } : null;
544
+ }
428
545
  // ── Tool-call summarization (ported from pikiloom's summarizeClaudeToolUse) ──────────
429
546
  // Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
430
547
  // activity projector joins these into snapshot.activity; the structured form lives in
@@ -241,7 +241,21 @@ export class CodexDriver {
241
241
  const ok = await srv.start();
242
242
  if (!ok)
243
243
  return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
244
- const onAbort = () => srv.kill();
244
+ let settle = () => { };
245
+ const turnDone = new Promise((res) => { settle = res; });
246
+ // On abort: gracefully interrupt the running turn, then settle turnDone OURSELVES. A bare
247
+ // srv.kill() (SIGTERM) never produces a turn/completed notification, so without this explicit
248
+ // settle() the `await turnDone` below hangs forever — run() never resolves and the task stays
249
+ // "running" in the orchestrator even though the codex process is already dead ("停止不掉,但实际上已经停了").
250
+ const onAbort = () => {
251
+ if (state.sessionId && state.turnId) {
252
+ srv.call('turn/interrupt', { threadId: state.sessionId, turnId: state.turnId }, 5_000).finally(() => settle());
253
+ }
254
+ else {
255
+ srv.kill();
256
+ settle();
257
+ }
258
+ };
245
259
  if (ctx.signal.aborted)
246
260
  onAbort();
247
261
  else
@@ -264,8 +278,6 @@ export class CodexDriver {
264
278
  state.sessionId = threadId;
265
279
  ctx.emit({ type: 'session', sessionId: threadId });
266
280
  }
267
- let settle = () => { };
268
- const turnDone = new Promise((res) => { settle = res; });
269
281
  srv.onNotification((method, params) => {
270
282
  if (params?.threadId && params.threadId !== state.sessionId && method !== 'turn/started')
271
283
  return;