picocode-core 0.9.155 → 0.9.157
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 +1 -1
- package/src/agent.js +21 -1
- package/src/attachments.js +7 -0
- package/src/controller.js +40 -28
- package/src/derive.js +1 -1
- package/src/pending.js +8 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
2
4
|
import { compose, scope, model, noToolsCalled, Inherit, getText } from '@prsm/ai'
|
|
3
|
-
import { fileLabel, mediaTypeFor, selectionLabel } from './attachments.js'
|
|
5
|
+
import { commitLabel, fileLabel, mediaTypeFor, selectionLabel } from './attachments.js'
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
const COMMIT_PATCH_LIMIT = 60 * 1024
|
|
9
|
+
|
|
10
|
+
// a commit part carries the commit itself: its header, stat, and patch,
|
|
11
|
+
// cut off past the limit, so the model reads what happened without a
|
|
12
|
+
// git call of its own
|
|
13
|
+
export async function hydrateCommit(part) {
|
|
14
|
+
const head = commitLabel(part)
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await exec('git', ['--no-optional-locks', 'show', '--stat', '--patch', '--format=%H%nAuthor: %an <%ae>%nDate: %ad%n%n%B', part.hash], { cwd: part.root, maxBuffer: COMMIT_PATCH_LIMIT * 4 })
|
|
17
|
+
const body = stdout.length > COMMIT_PATCH_LIMIT ? `${stdout.slice(0, COMMIT_PATCH_LIMIT)}\n[patch truncated at ${COMMIT_PATCH_LIMIT} bytes]` : stdout
|
|
18
|
+
return { type: 'text', text: `${head}\n${body.trimEnd()}\n[/commit]` }
|
|
19
|
+
} catch {
|
|
20
|
+
return { type: 'text', text: `${head}\n[commit unavailable]\n[/commit]` }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
// file parts reach the model as a labelled path; it reads them itself
|
|
6
25
|
async function hydratePart(part) {
|
|
7
26
|
if (part.type === 'file') return { type: 'text', text: fileLabel(part.path) }
|
|
8
27
|
if (part.type === 'selection') return { type: 'text', text: selectionLabel(part) }
|
|
28
|
+
if (part.type === 'commit') return hydrateCommit(part)
|
|
9
29
|
if (part.type !== 'image' || part.source?.kind !== 'path') return part
|
|
10
30
|
const mediaType = part.source.mediaType || mediaTypeFor(part.source.path)
|
|
11
31
|
if (!mediaType) return { type: 'text', text: `[image unavailable: ${part.source.path}]` }
|
package/src/attachments.js
CHANGED
|
@@ -72,6 +72,7 @@ export function splitTextByImagePaths(text, exists = existsSync) {
|
|
|
72
72
|
|
|
73
73
|
export const fileLabel = (path) => `[file: ${path}]`
|
|
74
74
|
export const selectionLabel = (part) => `[selection: ${part.path}:${part.fromLine}-${part.toLine}]\n${part.text}\n[/selection]`
|
|
75
|
+
export const commitLabel = (part) => `[commit: ${part.hash.slice(0, 7)} ${JSON.stringify(part.subject ?? '')}]`
|
|
75
76
|
|
|
76
77
|
// [Image #n] and [File #n] placeholders in the composed text stand for
|
|
77
78
|
// attachments; they become image, file, or selection parts of the user message
|
|
@@ -86,6 +87,8 @@ export function buildUserContent(text, attachments) {
|
|
|
86
87
|
if (before) parts.push({ type: 'text', text: before })
|
|
87
88
|
parts.push(attachment.kind === 'selection'
|
|
88
89
|
? { type: 'selection', path: attachment.path, text: attachment.text, fromLine: attachment.fromLine, toLine: attachment.toLine, ...(Number.isInteger(attachment.fromColumn) ? { fromColumn: attachment.fromColumn } : {}), ...(Number.isInteger(attachment.toColumn) ? { toColumn: attachment.toColumn } : {}) }
|
|
90
|
+
: attachment.kind === 'commit'
|
|
91
|
+
? { type: 'commit', hash: attachment.hash, subject: attachment.subject, root: attachment.root }
|
|
89
92
|
: attachment.kind === 'file'
|
|
90
93
|
? { type: 'file', path: attachment.path }
|
|
91
94
|
: { type: 'image', source: { kind: 'path', path: attachment.path, mediaType: attachment.mediaType } })
|
|
@@ -128,6 +131,10 @@ export function inputTextFromContent(content, { attachments, nextId }) {
|
|
|
128
131
|
const placeholder = `[File #${nextId()}]`
|
|
129
132
|
attachments.set(placeholder, { path: part.path, kind: 'file' })
|
|
130
133
|
out += placeholder
|
|
134
|
+
} else if (part.type === 'commit' && part.hash) {
|
|
135
|
+
const placeholder = `[File #${nextId()}]`
|
|
136
|
+
attachments.set(placeholder, { hash: part.hash, subject: part.subject, root: part.root, kind: 'commit' })
|
|
137
|
+
out += placeholder
|
|
131
138
|
} else if (part.type === 'selection' && part.path) {
|
|
132
139
|
const placeholder = `[File #${nextId()}]`
|
|
133
140
|
attachments.set(placeholder, { path: part.path, text: part.text, fromLine: part.fromLine, toLine: part.toLine, ...(Number.isInteger(part.fromColumn) ? { fromColumn: part.fromColumn } : {}), ...(Number.isInteger(part.toColumn) ? { toColumn: part.toColumn } : {}), kind: 'selection' })
|
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
|
|
400
|
-
//
|
|
401
|
-
//
|
|
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(
|
|
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
|
|
468
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
executeTurn(
|
|
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
|
-
|
|
862
|
-
set({ expedited: [...state.expedited, `${label}\n${placeholder}`] })
|
|
864
|
+
set({ views: [...state.views, `${label}\n${placeholder}`] })
|
|
863
865
|
return true
|
|
864
866
|
}
|
|
865
867
|
|
|
@@ -877,6 +879,15 @@ export function createController({ boot }) {
|
|
|
877
879
|
return placeholder
|
|
878
880
|
}
|
|
879
881
|
|
|
882
|
+
// a commit of the session's repository; the model receives the commit
|
|
883
|
+
// itself when the message is sent
|
|
884
|
+
function attachCommit({ hash, subject = '' }) {
|
|
885
|
+
if (!/^[0-9a-f]{7,40}$/i.test(String(hash ?? ''))) return null
|
|
886
|
+
const placeholder = `[File #${++state.imageCount}]`
|
|
887
|
+
state.attachments.set(placeholder, { hash, subject: String(subject), root: boot.root, kind: 'commit' })
|
|
888
|
+
return placeholder
|
|
889
|
+
}
|
|
890
|
+
|
|
880
891
|
// a project-relative pick: images attach as images, anything else as a
|
|
881
892
|
// file reference
|
|
882
893
|
function attachProjectFile(file) {
|
|
@@ -949,7 +960,7 @@ export function createController({ boot }) {
|
|
|
949
960
|
boot.git.retarget(next.root)
|
|
950
961
|
next.mcp.connectAll()
|
|
951
962
|
emit('mcp', next.mcp.list())
|
|
952
|
-
set({ queued: [], expedited: [] })
|
|
963
|
+
set({ queued: [], expedited: [], views: [] })
|
|
953
964
|
emit('project', boot)
|
|
954
965
|
await resume(meta)
|
|
955
966
|
flash(`switched to ${next.displayCwd}`)
|
|
@@ -1372,6 +1383,7 @@ export function createController({ boot }) {
|
|
|
1372
1383
|
attachImage,
|
|
1373
1384
|
attachFile,
|
|
1374
1385
|
attachSelection,
|
|
1386
|
+
attachCommit,
|
|
1375
1387
|
attachProjectFile,
|
|
1376
1388
|
detachImage,
|
|
1377
1389
|
costSummary,
|
package/src/derive.js
CHANGED
|
@@ -101,7 +101,7 @@ function foldMessage(state, event) {
|
|
|
101
101
|
if (message.role === 'user') {
|
|
102
102
|
const text = Array.isArray(message.content)
|
|
103
103
|
? message.content
|
|
104
|
-
.map((p) => (p.type === 'text' ? p.text : p.type === 'file' ? `[file: ${p.path}]` : p.type === 'selection' ? `[selection: ${p.path}:${p.fromLine}-${p.toLine}]` : `[image: ${String(p.source?.path || '').split('/').pop() || 'attached'}]`))
|
|
104
|
+
.map((p) => (p.type === 'text' ? p.text : p.type === 'file' ? `[file: ${p.path}]` : p.type === 'selection' ? `[selection: ${p.path}:${p.fromLine}-${p.toLine}]` : p.type === 'commit' ? `[commit: ${String(p.hash).slice(0, 7)}]` : `[image: ${String(p.source?.path || '').split('/').pop() || 'attached'}]`))
|
|
105
105
|
.join('')
|
|
106
106
|
: String(message.content)
|
|
107
107
|
state.transcript.push({ ...base, kind: 'user', text, content: message.content, ...(event.data.origin ? { origin: event.data.origin } : {}) })
|
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
|
+
}
|