picocode-core 0.9.155 → 0.9.156

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocode-core",
3
- "version": "0.9.155",
3
+ "version": "0.9.156",
4
4
  "description": "The agent runtime behind pico: sessions, tools, subagents, MCP, memory, and model access",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/controller.js CHANGED
@@ -30,6 +30,7 @@ import { loadCodexModels } from './codex-models.js'
30
30
  import { fuzzyScore } from './fuzzy.js'
31
31
  import { MAX_DELIBERATION_ROUNDS } from './deliberation.js'
32
32
  import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor, stashImages } from './attachments.js'
33
+ import { settlePending } from './pending.js'
33
34
 
34
35
  export const EFFORT_LEVELS = [
35
36
  { key: null, desc: 'let the provider decide how much to think' },
@@ -118,6 +119,7 @@ export function createController({ boot }) {
118
119
  streaming: null,
119
120
  queued: [],
120
121
  expedited: [],
122
+ views: [],
121
123
  sent: [],
122
124
  question: null,
123
125
  rewindUndo: null,
@@ -396,16 +398,18 @@ export function createController({ boot }) {
396
398
  state.streaming = null
397
399
  }
398
400
 
399
- async function executeTurn(text) {
400
- // an image the model asked to view arrives with its path in the label;
401
- // that text must not be scanned for image paths or it attaches twice
402
- const built = buildUserContent(text, state.attachments)
403
- const viewed = built.used.length > 0 && built.used.every((placeholder) => deliveredImages.has(placeholder))
404
- const { content: built2, used } = viewed ? built : finalizeUserContent(text, state.attachments)
405
- for (const placeholder of used) deliveredImages.delete(placeholder)
401
+ async function persistUserMessage(text, { viewed = false } = {}) {
402
+ // a view label carries the image's path; only user text is scanned
403
+ // for paths, or the viewed image would attach twice
404
+ const { content: built } = viewed ? buildUserContent(text, state.attachments) : finalizeUserContent(text, state.attachments)
406
405
  ensureSession()
407
- const content = state.session.file ? await stashImages(built2, sessionAttachmentsDir(boot.root, state.session.id)) : built2
406
+ const content = state.session.file ? await stashImages(built, sessionAttachmentsDir(boot.root, state.session.id)) : built
408
407
  persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
408
+ }
409
+
410
+ async function executeTurn(text, { views = [] } = {}) {
411
+ for (const view of views) await persistUserMessage(view, { viewed: true })
412
+ if (text) await persistUserMessage(text)
409
413
  reDerive()
410
414
  await runAgentTurn()
411
415
  }
@@ -463,11 +467,12 @@ export function createController({ boot }) {
463
467
  set({ compacting: false, compactStatus: null, busy: false })
464
468
  }
465
469
 
466
- if (state.expedited.length > 0 || state.queued.length > 0) {
467
- const next = takePending()
468
- if (controller.signal.aborted) emit('input', next.join('\n'))
470
+ if (state.expedited.length > 0 || state.queued.length > 0 || state.views.length > 0) {
471
+ const settled = settlePending({ ...state, interrupted: controller.signal.aborted })
472
+ set({ expedited: [], queued: settled.queued, views: [] })
473
+ if (settled.recall.length > 0) emit('input', settled.recall.join('\n'))
469
474
  else {
470
- executeTurn(next.join('\n'))
475
+ executeTurn(settled.messages.join('\n'), { views: settled.views })
471
476
  return
472
477
  }
473
478
  }
@@ -571,7 +576,7 @@ export function createController({ boot }) {
571
576
  : item,
572
577
  ),
573
578
  )
574
- if (state.expedited.length > 0) {
579
+ if (state.expedited.length > 0 || state.views.length > 0) {
575
580
  sendAfterToolTriggered = true
576
581
  controller.abort()
577
582
  }
@@ -680,18 +685,15 @@ export function createController({ boot }) {
680
685
  )
681
686
  }
682
687
 
683
- const expeditedMessages = state.expedited
684
- const pendingMessages = state.queued
685
- if (expeditedMessages.length > 0 || pendingMessages.length > 0) {
686
- set({ expedited: [], queued: [] })
688
+ if (state.expedited.length > 0 || state.queued.length > 0 || state.views.length > 0) {
687
689
  // an interrupt is the user taking the wheel: nothing pending may
688
- // auto-send, expedited or not; it all returns to the composer
689
- if (result.interrupted && !sendAfterToolTriggered) {
690
- emit('input', [...expeditedMessages, ...pendingMessages].join('\n'))
691
- } else {
692
- const next = sendAfterToolTriggered ? expeditedMessages : [...expeditedMessages, ...pendingMessages]
693
- if (sendAfterToolTriggered && pendingMessages.length > 0) set({ queued: pendingMessages })
694
- executeTurn(next.join('\n'))
690
+ // auto-send, expedited or not; it all returns to the composer, and
691
+ // an image the model asked for is dropped with the turn
692
+ const settled = settlePending({ ...state, afterTool: sendAfterToolTriggered, interrupted: result.interrupted })
693
+ set({ expedited: [], queued: settled.queued, views: [] })
694
+ if (settled.recall.length > 0) emit('input', settled.recall.join('\n'))
695
+ else {
696
+ executeTurn(settled.messages.join('\n'), { views: settled.views })
695
697
  return
696
698
  }
697
699
  }
@@ -749,6 +751,7 @@ export function createController({ boot }) {
749
751
  state.rewindUndo = null
750
752
  state.queued = []
751
753
  state.expedited = []
754
+ state.views = []
752
755
  state.sent = []
753
756
  state.model = model ?? state.defaultModel
754
757
  state.effort = effort === undefined ? state.defaultEffort : effort
@@ -795,6 +798,7 @@ export function createController({ boot }) {
795
798
  state.rewindUndo = null
796
799
  state.queued = []
797
800
  state.expedited = []
801
+ state.views = []
798
802
  state.sent = []
799
803
  agents.restore(forked.events)
800
804
  reDerive()
@@ -854,12 +858,10 @@ export function createController({ boot }) {
854
858
 
855
859
  // an image the model asked to see rides in as an expedited user message,
856
860
  // which the tool-completion path sends right after the current call
857
- const deliveredImages = new Set()
858
861
  function deliverImage(path, label) {
859
862
  const placeholder = attachImage(path)
860
863
  if (!placeholder) return false
861
- deliveredImages.add(placeholder)
862
- set({ expedited: [...state.expedited, `${label}\n${placeholder}`] })
864
+ set({ views: [...state.views, `${label}\n${placeholder}`] })
863
865
  return true
864
866
  }
865
867
 
@@ -949,7 +951,7 @@ export function createController({ boot }) {
949
951
  boot.git.retarget(next.root)
950
952
  next.mcp.connectAll()
951
953
  emit('mcp', next.mcp.list())
952
- set({ queued: [], expedited: [] })
954
+ set({ queued: [], expedited: [], views: [] })
953
955
  emit('project', boot)
954
956
  await resume(meta)
955
957
  flash(`switched to ${next.displayCwd}`)
package/src/pending.js ADDED
@@ -0,0 +1,8 @@
1
+ // what happens to the messages waiting on a turn once it ends. images the
2
+ // model asked to view travel apart from the user's own text, so a view
3
+ // delivery is never folded into a user message
4
+ export function settlePending({ views = [], expedited = [], queued = [], afterTool = false, interrupted = false }) {
5
+ if (interrupted && !afterTool) return { views: [], messages: [], queued: [], recall: [...expedited, ...queued] }
6
+ if (afterTool) return { views, messages: expedited, queued, recall: [] }
7
+ return { views, messages: [...expedited, ...queued], queued: [], recall: [] }
8
+ }