@@ -17,7 +17,7 @@ import path from "node:path";
1717import { CANVASES , hashBoards } 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
@@ -327,6 +333,16 @@ export function createAgentServer(options: {
327333 ! / ^ [ A - Z a - z 0 - 9 + / ] * = { 0 , 2 } $ / . test ( i . data )
328334 )
329335 return send ( 400 , "bad image data" ) ;
336+ if (
337+ i . reference !== undefined &&
338+ ( typeof i . reference !== "object" ||
339+ ! [ "project" , "community" ] . every (
340+ ( k ) =>
341+ i . reference [ k ] === undefined ||
342+ typeof i . reference [ k ] === "string" ,
343+ ) )
344+ )
345+ return send ( 400 , "bad image reference" ) ;
330346 }
331347 // The body cap above is the panel's limit in base64; a client that is not the
332348 // panel meets the limit itself here, in the bytes the files come out as.
@@ -375,7 +391,7 @@ export function createAgentServer(options: {
375391 // The project it was sent from, by the name its address carries. None from the home page
376392 // or an example, and then the agent has no project to write to.
377393 const dir =
378- project === undefined ? undefined : projects ( ) . get ( project ) ;
394+ project === undefined ? undefined : projectDir ( project ) ;
379395 if ( project !== undefined && dir === undefined )
380396 return send ( 404 , "no such project" ) ;
381397 // The session the panel is in, or a new one. Its id names a folder and a file, so it has
@@ -511,15 +527,44 @@ export function createAgentServer(options: {
511527 fs . mkdirSync ( keptOf ( id ) , { recursive : true } ) ;
512528 const imagesDir = images . length ? keptOf ( id ) : "" ;
513529 const held : AgentImage [ ] = images . map (
514- ( i : { n : number ; name : string ; type : string ; data : string } ) => ( {
515- ...i ,
516- path : picture (
517- id ,
518- `image-${ i . n } ` ,
519- i . type ,
520- Buffer . from ( i . data , "base64" ) ,
521- ) ,
522- } ) ,
530+ ( i : {
531+ n : number ;
532+ name : string ;
533+ type : string ;
534+ data : string ;
535+ reference ?: { project ?: string ; community ?: string } ;
536+ } ) => {
537+ // Anything but a picture (a board, a video, a note) keeps its picture for the
538+ // panel, and the agent is pointed at its file, in the canvases of the project it
539+ // was attached from, which need not be the one it is sent from. A community
540+ // project's is not on this machine, so it keeps the name `sp fetch` finds it by.
541+ const ref = i . reference ;
542+ const from = ref ?. project && projects ( ) . get ( ref . project ) ;
543+ const file =
544+ ref ?. community === undefined &&
545+ ( ref ?. project === undefined || from )
546+ ? canvasFile (
547+ from ? path . join ( from , CANVASES ) : examplesDir ,
548+ examplesDir ,
549+ i . name ,
550+ )
551+ : undefined ;
552+ return {
553+ ...i ,
554+ path : picture (
555+ id ,
556+ `image-${ i . n } ` ,
557+ i . type ,
558+ Buffer . from ( i . data , "base64" ) ,
559+ ) ,
560+ reference :
561+ ref &&
562+ ( file ??
563+ ( ref . community
564+ ? `${ i . name } of the community project ${ ref . community } `
565+ : i . name ) ) ,
566+ } ;
567+ } ,
523568 ) ;
524569 const hashes = hashBoards ( boards ) ;
525570 const c = command (
@@ -541,11 +586,12 @@ export function createAgentServer(options: {
541586 // server's port.
542587 env : {
543588 ...process . env ,
544- SP_PROJECT : project ,
589+ SP_PROJECT : dir && path . basename ( dir ) ,
545590 SP_CANVAS_PORT : String ( req . socket . localPort ) ,
546591 } ,
547592 } ) ,
548593 stopped : false ,
594+ dir,
549595 } ) ;
550596 runs . set ( run . id , run ) ;
551597 boardsBefore . set ( run . id , { dir : boards , hashes } ) ;
@@ -656,6 +702,24 @@ export function createAgentServer(options: {
656702 finish ( error . code === "ENOENT" ? def . missing : String ( error ) ) ;
657703 } ) ;
658704 run . child . on ( "close" , ( code , signal ) => {
705+ // The turn that named an unnamed project is over, so its folder can take the name.
706+ // Not when another turn has already started on it: that one moves it when it ends.
707+ if (
708+ dir !== undefined &&
709+ code === 0 &&
710+ ! [ ...runs . values ( ) ] . some ( ( r ) => r !== run && r . dir === dir && ! ended ( r ) )
711+ ) {
712+ // Never in the way of the run ending: a throw here would leave it running for good.
713+ try {
714+ const moved = named ( dir ) ;
715+ if ( moved )
716+ record . projects = record . projects . map ( ( had ) =>
717+ had === dir ? moved : had ,
718+ ) ;
719+ } catch ( error ) {
720+ console . error ( `[agent] ${ dir } could not be named: ${ error } ` ) ;
721+ }
722+ }
659723 settle ( ) ;
660724 if ( ended ( run ) ) return ;
661725 const tail = stderr . trim ( ) . split ( "\n" ) . slice ( - 5 ) . join ( "\n" ) ;
0 commit comments