picocode-core 0.9.156 → 0.9.158
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 +32 -1
- package/src/attachments.js +7 -0
- package/src/controller.js +10 -0
- package/src/derive.js +1 -1
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
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
|
+
// a whole patch rides along only while it is small; a bigger commit
|
|
9
|
+
// arrives as its header and stat, and the model pulls the files it cares
|
|
10
|
+
// about with git show. 8KB is a couple of thousand tokens
|
|
11
|
+
const COMMIT_PATCH_LIMIT = 8 * 1024
|
|
12
|
+
const COMMIT_STAT_FILES = 80
|
|
13
|
+
const GIT_SHOW = ['--no-optional-locks', 'show', '--format=%H%nAuthor: %an <%ae>%nDate: %ad%n%n%B']
|
|
14
|
+
|
|
15
|
+
function trimStat(stat) {
|
|
16
|
+
const lines = stat.trimEnd().split('\n')
|
|
17
|
+
if (lines.length <= COMMIT_STAT_FILES + 1) return stat.trimEnd()
|
|
18
|
+
return [...lines.slice(0, COMMIT_STAT_FILES), `... ${lines.length - COMMIT_STAT_FILES - 1} more files`, lines.at(-1)].join('\n')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function hydrateCommit(part) {
|
|
22
|
+
const head = commitLabel(part)
|
|
23
|
+
const options = { cwd: part.root, maxBuffer: 64 * 1024 * 1024 }
|
|
24
|
+
try {
|
|
25
|
+
const { stdout: full } = await exec('git', [...GIT_SHOW, '--stat', '--patch', part.hash], options)
|
|
26
|
+
if (full.length <= COMMIT_PATCH_LIMIT) return { type: 'text', text: `${head}\n${full.trimEnd()}\n[/commit]` }
|
|
27
|
+
const { stdout: stat } = await exec('git', [...GIT_SHOW, '--stat', part.hash], options)
|
|
28
|
+
const note = `[patch omitted: ${Math.round(full.length / 1024)}KB; run git show ${part.hash.slice(0, 7)} -- <path> for the files you need]`
|
|
29
|
+
return { type: 'text', text: `${head}\n${trimStat(stat)}\n${note}\n[/commit]` }
|
|
30
|
+
} catch {
|
|
31
|
+
return { type: 'text', text: `${head}\n[commit unavailable]\n[/commit]` }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
4
34
|
|
|
5
35
|
// file parts reach the model as a labelled path; it reads them itself
|
|
6
36
|
async function hydratePart(part) {
|
|
7
37
|
if (part.type === 'file') return { type: 'text', text: fileLabel(part.path) }
|
|
8
38
|
if (part.type === 'selection') return { type: 'text', text: selectionLabel(part) }
|
|
39
|
+
if (part.type === 'commit') return hydrateCommit(part)
|
|
9
40
|
if (part.type !== 'image' || part.source?.kind !== 'path') return part
|
|
10
41
|
const mediaType = part.source.mediaType || mediaTypeFor(part.source.path)
|
|
11
42
|
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
|
@@ -879,6 +879,15 @@ export function createController({ boot }) {
|
|
|
879
879
|
return placeholder
|
|
880
880
|
}
|
|
881
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
|
+
|
|
882
891
|
// a project-relative pick: images attach as images, anything else as a
|
|
883
892
|
// file reference
|
|
884
893
|
function attachProjectFile(file) {
|
|
@@ -1374,6 +1383,7 @@ export function createController({ boot }) {
|
|
|
1374
1383
|
attachImage,
|
|
1375
1384
|
attachFile,
|
|
1376
1385
|
attachSelection,
|
|
1386
|
+
attachCommit,
|
|
1377
1387
|
attachProjectFile,
|
|
1378
1388
|
detachImage,
|
|
1379
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 } : {}) })
|