@@ -17,7 +17,7 @@ import path from "node:path";
1717import { CANVASES } from "./boards.ts" ;
1818import { command , stop } from "./command.ts" ;
1919import { AGENT_SKILLS , installSkills } from "./skills.ts" ;
20- import { folderOf , sameOrigin } from "./sp.ts" ;
20+ import { canvasFile , folderOf , sameOrigin } from "./sp.ts" ;
2121import { SAFE_NAME } from "../src/layoutEdit.ts" ;
2222import {
2323 attach ,
@@ -45,12 +45,16 @@ export function createAgentServer(options: {
4545 examplesDir : string ;
4646 /** Every project by the name its address carries, `/p/<name>/`. */
4747 projects : ( ) => Map < string , string > ;
48+ /** Moves a project the agent has just named into a folder of that name (projects.ts). */
49+ named : ( dir : string ) => string | undefined ;
50+ /** A project by name, or by the name it had before `named` moved it. */
51+ project : ( name : string ) => string | undefined ;
4852 /** This plugin's checkout, whose skills the sessions are given. */
4953 repoRoot : string ;
5054 /** `<projects dir>/.workspaces`: a folder per session, a record beside each, and the skills. */
5155 workspaces : string ;
5256} ) {
53- const { examplesDir, projects, repoRoot, workspaces } = options ;
57+ const { examplesDir, named , project : projectDir , projects, repoRoot, workspaces } = options ;
5458 // One process per message — Claude Code or Codex, by the panel's choice, looked up in
5559 // agents.ts — its output kept here and streamed to the page. The runs are held in memory, the
5660 // newest twenty, for the panel to follow and for the history to say which session is running,
@@ -68,6 +72,8 @@ export function createAgentServer(options: {
6872 child : ChildProcess ;
6973 /** The Stop button was pressed. Windows ends a run by exit code 1, which says nothing. */
7074 stopped : boolean ;
75+ /** The project it works on, if any. */
76+ dir : string | undefined ;
7177 }
7278 > ( ) ;
7379
@@ -310,6 +316,16 @@ export function createAgentServer(options: {
310316 ! / ^ [ A - Z a - z 0 - 9 + / ] * = { 0 , 2 } $ / . test ( i . data )
311317 )
312318 return send ( 400 , "bad image data" ) ;
319+ if (
320+ i . reference !== undefined &&
321+ ( typeof i . reference !== "object" ||
322+ ! [ "project" , "community" ] . every (
323+ ( k ) =>
324+ i . reference [ k ] === undefined ||
325+ typeof i . reference [ k ] === "string" ,
326+ ) )
327+ )
328+ return send ( 400 , "bad image reference" ) ;
313329 }
314330 // The body cap above is the panel's limit in base64; a client that is not the
315331 // panel meets the limit itself here, in the bytes the files come out as.
@@ -358,7 +374,7 @@ export function createAgentServer(options: {
358374 // The project it was sent from, by the name its address carries. None from the home page
359375 // or an example, and then the agent has no project to write to.
360376 const dir =
361- project === undefined ? undefined : projects ( ) . get ( project ) ;
377+ project === undefined ? undefined : projectDir ( project ) ;
362378 if ( project !== undefined && dir === undefined )
363379 return send ( 404 , "no such project" ) ;
364380 // The session the panel is in, or a new one. Its id names a folder and a file, so it has
@@ -494,15 +510,44 @@ export function createAgentServer(options: {
494510 fs . mkdirSync ( keptOf ( id ) , { recursive : true } ) ;
495511 const imagesDir = images . length ? keptOf ( id ) : "" ;
496512 const held : AgentImage [ ] = images . map (
497- ( i : { n : number ; name : string ; type : string ; data : string } ) => ( {
498- ...i ,
499- path : picture (
500- id ,
501- `image-${ i . n } ` ,
502- i . type ,
503- Buffer . from ( i . data , "base64" ) ,
504- ) ,
505- } ) ,
513+ ( i : {
514+ n : number ;
515+ name : string ;
516+ type : string ;
517+ data : string ;
518+ reference ?: { project ?: string ; community ?: string } ;
519+ } ) => {
520+ // Anything but a picture (a board, a video, a note) keeps its picture for the
521+ // panel, and the agent is pointed at its file, in the canvases of the project it
522+ // was attached from, which need not be the one it is sent from. A community
523+ // project's is not on this machine, so it keeps the name `sp fetch` finds it by.
524+ const ref = i . reference ;
525+ const from = ref ?. project && projects ( ) . get ( ref . project ) ;
526+ const file =
527+ ref ?. community === undefined &&
528+ ( ref ?. project === undefined || from )
529+ ? canvasFile (
530+ from ? path . join ( from , CANVASES ) : examplesDir ,
531+ examplesDir ,
532+ i . name ,
533+ )
534+ : undefined ;
535+ return {
536+ ...i ,
537+ path : picture (
538+ id ,
539+ `image-${ i . n } ` ,
540+ i . type ,
541+ Buffer . from ( i . data , "base64" ) ,
542+ ) ,
543+ reference :
544+ ref &&
545+ ( file ??
546+ ( ref . community
547+ ? `${ i . name } of the community project ${ ref . community } `
548+ : i . name ) ) ,
549+ } ;
550+ } ,
506551 ) ;
507552 const c = command (
508553 def . bin ,
@@ -523,11 +568,12 @@ export function createAgentServer(options: {
523568 // server's port.
524569 env : {
525570 ...process . env ,
526- SP_PROJECT : project ,
571+ SP_PROJECT : dir && path . basename ( dir ) ,
527572 SP_CANVAS_PORT : String ( req . socket . localPort ) ,
528573 } ,
529574 } ) ,
530575 stopped : false ,
576+ dir,
531577 } ) ;
532578 runs . set ( run . id , run ) ;
533579 record . runs . push ( run . id ) ;
@@ -637,6 +683,24 @@ export function createAgentServer(options: {
637683 finish ( error . code === "ENOENT" ? def . missing : String ( error ) ) ;
638684 } ) ;
639685 run . child . on ( "close" , ( code , signal ) => {
686+ // The turn that named an unnamed project is over, so its folder can take the name.
687+ // Not when another turn has already started on it: that one moves it when it ends.
688+ if (
689+ dir !== undefined &&
690+ code === 0 &&
691+ ! [ ...runs . values ( ) ] . some ( ( r ) => r !== run && r . dir === dir && ! ended ( r ) )
692+ ) {
693+ // Never in the way of the run ending: a throw here would leave it running for good.
694+ try {
695+ const moved = named ( dir ) ;
696+ if ( moved )
697+ record . projects = record . projects . map ( ( had ) =>
698+ had === dir ? moved : had ,
699+ ) ;
700+ } catch ( error ) {
701+ console . error ( `[agent] ${ dir } could not be named: ${ error } ` ) ;
702+ }
703+ }
640704 settle ( ) ;
641705 if ( ended ( run ) ) return ;
642706 const tail = stderr . trim ( ) . split ( "\n" ) . slice ( - 5 ) . join ( "\n" ) ;
0 commit comments