picocode-core 0.9.144 → 0.9.146

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.144",
3
+ "version": "0.9.146",
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,10 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { compose, scope, model, noToolsCalled, Inherit, getText } from '@prsm/ai'
3
- import { fileLabel, mediaTypeFor } from './attachments.js'
3
+ import { fileLabel, mediaTypeFor, selectionLabel } from './attachments.js'
4
4
 
5
5
  // file parts reach the model as a labelled path; it reads them itself
6
6
  async function hydratePart(part) {
7
7
  if (part.type === 'file') return { type: 'text', text: fileLabel(part.path) }
8
+ if (part.type === 'selection') return { type: 'text', text: selectionLabel(part) }
8
9
  if (part.type !== 'image' || part.source?.kind !== 'path') return part
9
10
  const mediaType = part.source.mediaType || mediaTypeFor(part.source.path)
10
11
  if (!mediaType) return { type: 'text', text: `[image unavailable: ${part.source.path}]` }
@@ -71,9 +71,10 @@ export function splitTextByImagePaths(text, exists = existsSync) {
71
71
  }
72
72
 
73
73
  export const fileLabel = (path) => `[file: ${path}]`
74
+ export const selectionLabel = (part) => `[selection: ${part.path}:${part.fromLine}-${part.toLine}]\n${part.text}\n[/selection]`
74
75
 
75
76
  // [Image #n] and [File #n] placeholders in the composed text stand for
76
- // attachments; they become image and file parts of the user message
77
+ // attachments; they become image, file, or selection parts of the user message
77
78
  export function buildUserContent(text, attachments) {
78
79
  const parts = []
79
80
  let last = 0
@@ -83,9 +84,11 @@ export function buildUserContent(text, attachments) {
83
84
  if (!attachment) continue
84
85
  const before = text.slice(last, match.index)
85
86
  if (before) parts.push({ type: 'text', text: before })
86
- parts.push(attachment.kind === 'file'
87
- ? { type: 'file', path: attachment.path }
88
- : { type: 'image', source: { kind: 'path', path: attachment.path, mediaType: attachment.mediaType } })
87
+ parts.push(attachment.kind === 'selection'
88
+ ? { 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 } : {}) }
89
+ : attachment.kind === 'file'
90
+ ? { type: 'file', path: attachment.path }
91
+ : { type: 'image', source: { kind: 'path', path: attachment.path, mediaType: attachment.mediaType } })
89
92
  used.push(match[0])
90
93
  last = match.index + match[0].length
91
94
  }
@@ -125,6 +128,10 @@ export function inputTextFromContent(content, { attachments, nextId }) {
125
128
  const placeholder = `[File #${nextId()}]`
126
129
  attachments.set(placeholder, { path: part.path, kind: 'file' })
127
130
  out += placeholder
131
+ } else if (part.type === 'selection' && part.path) {
132
+ const placeholder = `[File #${nextId()}]`
133
+ 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' })
134
+ out += placeholder
128
135
  } else {
129
136
  out += '[image]'
130
137
  }
package/src/controller.js CHANGED
@@ -857,6 +857,13 @@ export function createController({ boot }) {
857
857
  return placeholder
858
858
  }
859
859
 
860
+ function attachSelection({ path, text, fromLine, toLine, fromColumn, toColumn }) {
861
+ if (!existsSync(path) || !text || !Number.isInteger(fromLine) || !Number.isInteger(toLine)) return null
862
+ const placeholder = `[File #${++state.imageCount}]`
863
+ state.attachments.set(placeholder, { path, text, fromLine, toLine, fromColumn, toColumn, kind: 'selection' })
864
+ return placeholder
865
+ }
866
+
860
867
  // a project-relative pick: images attach as images, anything else as a
861
868
  // file reference
862
869
  function attachProjectFile(file) {
@@ -1325,6 +1332,7 @@ export function createController({ boot }) {
1325
1332
  recallText,
1326
1333
  attachImage,
1327
1334
  attachFile,
1335
+ attachSelection,
1328
1336
  attachProjectFile,
1329
1337
  detachImage,
1330
1338
  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}]` : `[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}]` : `[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/prompts.js ADDED
@@ -0,0 +1,41 @@
1
+ import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { picoHome } from './paths.js'
4
+ import { parseFrontmatter } from './skills.js'
5
+
6
+ export function globalPromptsDir() {
7
+ return join(picoHome(), 'prompts')
8
+ }
9
+
10
+ export function promptName(name) {
11
+ return String(name ?? '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '')
12
+ }
13
+
14
+ const promptFile = (name) => join(globalPromptsDir(), `${promptName(name)}.md`)
15
+
16
+ export async function listPrompts() {
17
+ let files = []
18
+ try { files = await readdir(globalPromptsDir()) } catch { return [] }
19
+ return Promise.all(files.filter((file) => file.endsWith('.md')).sort().map(async (file) => {
20
+ const text = await readFile(join(globalPromptsDir(), file), 'utf-8')
21
+ const { meta, body } = parseFrontmatter(text)
22
+ return { name: file.slice(0, -3), description: meta.description || '', body: body.trim() }
23
+ }))
24
+ }
25
+
26
+ export async function savePrompt({ previous, name, description, body }) {
27
+ const slug = promptName(name)
28
+ if (!slug || !String(body ?? '').trim()) throw new Error('prompt name and text are required')
29
+ await mkdir(globalPromptsDir(), { recursive: true })
30
+ const summary = String(description ?? '').trim().replace(/\s*\n\s*/g, ' ')
31
+ const text = `---\ndescription: ${summary}\n---\n\n${String(body).trim()}\n`
32
+ await writeFile(promptFile(slug), text)
33
+ const old = promptName(previous)
34
+ if (old && old !== slug) await unlink(promptFile(old)).catch(() => {})
35
+ return true
36
+ }
37
+
38
+ export async function removePrompt(name) {
39
+ await unlink(promptFile(name))
40
+ return true
41
+ }