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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocode-core",
3
- "version": "0.9.156",
3
+ "version": "0.9.158",
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/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}]` }
@@ -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 } : {}) })