@edspencer/paddock 0.59.0 → 0.60.0
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 +2 -2
- package/packages/server/dist/adoptable.d.ts +72 -3
- package/packages/server/dist/adoptable.js +231 -25
- package/packages/server/dist/cli/args.d.ts +56 -0
- package/packages/server/dist/cli/args.js +159 -0
- package/packages/server/dist/cli/paddock.d.ts +1 -30
- package/packages/server/dist/cli/paddock.js +6 -146
- package/packages/server/dist/herdctl.d.ts +80 -0
- package/packages/server/dist/herdctl.js +159 -0
- package/packages/server/dist/routes/chats.js +59 -3
- package/packages/server/dist/run-provenance.d.ts +11 -0
- package/packages/server/dist/run-provenance.js +18 -0
- package/packages/server/dist/ws-turn.d.ts +0 -13
- package/packages/server/dist/ws-turn.js +50 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-Cli_WHsM.js → InstanceConfigPage-xn9Rvoah.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-BYK9nmr4.js → MarkdownRenderer-DMONssKA.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DWcMpdZJ.js → ProjectRedirect-DHoWtvtY.js} +1 -1
- package/packages/web/dist/assets/ProjectView-BmSdUwY2.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-Bf2cx9-Z.js → ProjectsGrid-CjfwpeFj.js} +1 -1
- package/packages/web/dist/assets/{arc-CZ9CFd-g.js → arc-DdFdbD-c.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BzOXUlLu.js → architectureDiagram-3BPJPVTR-DC8UKtwV.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Bnkcx-Fn.js → blockDiagram-GPEHLZMM-DqFFe_5c.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-C-nqckPS.js → c4Diagram-AAUBKEIU-DbffFSM-.js} +1 -1
- package/packages/web/dist/assets/channel-QxsJGL6F.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-jVn7J_5N.js → chunk-2J33WTMH-8PP2RHft.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-D5eys7Hi.js → chunk-4BX2VUAB-Hag68B5N.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CqpdXIoL.js → chunk-55IACEB6-BXp_gQIa.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-SQikk09l.js → chunk-727SXJPM-DlntjQux.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-rfBNpUwD.js → chunk-AQP2D5EJ-BhI9KPrE.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-CnL0tA20.js → chunk-FMBD7UC4-WhfORs2z.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CZGi4swC.js → chunk-ND2GUHAM-CIskmv20.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-Du4WKuCj.js → chunk-QZHKN3VN-CNOTpCW4.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CI7p84hR.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CI7p84hR.js +2 -0
- package/packages/web/dist/assets/{core-LJ--cHnT.js → core-usOVRkLM.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CEovKrYZ.js → cose-bilkent-S5V4N54A-CniuMIIW.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-jQhK8147.js → dagre-BM42HDAG-5CteYfQY.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BmiauiWQ.js → diagram-2AECGRRQ-BIQjD2LA.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-VYdTkqGm.js → diagram-5GNKFQAL-1vy3ErTt.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DcOTKmwo.js → diagram-KO2AKTUF-D45iqFk6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DaehIKcD.js → diagram-LMA3HP47-DNktDktn.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DTqk6D4N.js → diagram-OG6HWLK6-BQ9JgV70.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Dj4rI8M1.js → erDiagram-TEJ5UH35-oBasZ1uv.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-D0FWSohH.js → flowDiagram-I6XJVG4X-DxnJHMBS.js} +1 -1
- package/packages/web/dist/assets/{format-CgkZTf5F.js → format-CIAat6sU.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DgaRh12R.js → ganttDiagram-6RSMTGT7-CtJQ2m7z.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5qiry4ao.js → gitGraphDiagram-PVQCEYII-5RPscwD_.js} +1 -1
- package/packages/web/dist/assets/index-B6etJCHf.css +1 -0
- package/packages/web/dist/assets/{index-CcYeBUNk.js → index-DtRblEY_.js} +4 -4
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CZmDwouw.js → infoDiagram-5YYISTIA-zQu3xkW4.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-knkHstDa.js → ishikawaDiagram-YF4QCWOH-ChoEjcdw.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-ByfdmIVy.js → journeyDiagram-JHISSGLW-DM2KWmDr.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DSzly2xb.js → kanban-definition-UN3LZRKU-DWzTpYgh.js} +1 -1
- package/packages/web/dist/assets/{linear-BX6qYIpB.js → linear-Bpy9Aj-K.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-BXPz6_-1.js → mermaid.core-D43x5bFb.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-fzfZWy91.js → mindmap-definition-RKZ34NQL-BhceB-cv.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-M99j1K9t.js → pieDiagram-4H26LBE5-BgcGlFFA.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B7fKq1ce.js → quadrantDiagram-W4KKPZXB-ZRVaOoSp.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-xCBFrXav.js → requirementDiagram-4Y6WPE33-2hHcJHBt.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Dp9KLMhd.js → sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CNNVWpta.js → sequenceDiagram-3UESZ5HK-D4BXQSSM.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CzKRXREb.js → stateDiagram-AJRCARHV-DftS7dIo.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CgBC1jpO.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DfvQTe5J.js → timeline-definition-PNZ67QCA-C0OAxyWY.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-2PBM01eq.js → vennDiagram-CIIHVFJN-C94fINN3.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DC1EIjci.js → wardley-L42UT6IY-CbFrlwHE.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-4kYoKkfS.js → wardleyDiagram-YWT4CUSO-CHG2nndd.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bin8O1V8.js → xychartDiagram-2RQKCTM6-U9ESc19z.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/ProjectView-CvD9WQsW.js +0 -13
- package/packages/web/dist/assets/channel-By51cUN7.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-sIDgFl-L.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-sIDgFl-L.js +0 -2
- package/packages/web/dist/assets/index-CvGiQddO.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-LKJwobEd.js +0 -2
|
@@ -191,6 +191,24 @@ export class HerdctlService {
|
|
|
191
191
|
* {@link cancel}). Entries are removed when the turn's stream ends.
|
|
192
192
|
*/
|
|
193
193
|
liveSessions = new Map();
|
|
194
|
+
/**
|
|
195
|
+
* Background-phase turns keyed by the synthetic job id the sink minted for
|
|
196
|
+
* them, valued by the chat session they belong to (paddock#528).
|
|
197
|
+
*
|
|
198
|
+
* After a turn's primary `result` lands, {@link chatSession} returns and drops
|
|
199
|
+
* its {@link liveSessions} entry — but the session itself may live on, held
|
|
200
|
+
* open by the reaper because the turn launched background work, streaming
|
|
201
|
+
* autonomous re-invocation turns. Those turns are real, running, and shown as
|
|
202
|
+
* such; until now they were also uncancellable, because nothing registered
|
|
203
|
+
* them anywhere Stop could reach.
|
|
204
|
+
*
|
|
205
|
+
* `interrupt()` would be the wrong tool even if it could: it targets an
|
|
206
|
+
* in-flight model turn, and the wedge case is a session sitting idle holding
|
|
207
|
+
* background work that will never finish. So these route to a force-reap
|
|
208
|
+
* instead — end the session, let the stream end, and let the ordinary unwind
|
|
209
|
+
* emit `chat:complete`. Entries are removed when the background stream ends.
|
|
210
|
+
*/
|
|
211
|
+
backgroundTurns = new Map();
|
|
194
212
|
/**
|
|
195
213
|
* Incremental index of `<stateDir>/jobs`, backing the unread-badge reads
|
|
196
214
|
* (`lastTurnCompletedAt*`) — one per service, reused across requests so a warm
|
|
@@ -528,7 +546,29 @@ export class HerdctlService {
|
|
|
528
546
|
: summary.sources.map((s) => s.sourceCwd);
|
|
529
547
|
// sessionId -> why detection withheld it.
|
|
530
548
|
const withheld = new Map(summary.filtered.map((f) => [f.sessionId, f.reason]));
|
|
549
|
+
// A selection narrows the offer the same way the noise filter does, and for
|
|
550
|
+
// the same reason: the engine adopts a whole SOURCE, so anything the user did
|
|
551
|
+
// not tick has to be released again afterwards. Tracked separately from
|
|
552
|
+
// `withheld` because a session the user simply did not choose is not a
|
|
553
|
+
// "skip" worth reporting back — it was never part of this import, and listing
|
|
554
|
+
// 23 of them would read as an import that half failed.
|
|
555
|
+
const deselected = opts.sessionIds === undefined
|
|
556
|
+
? null
|
|
557
|
+
: (() => {
|
|
558
|
+
const wanted = new Set(opts.sessionIds);
|
|
559
|
+
const out = new Set();
|
|
560
|
+
for (const source of summary.sources) {
|
|
561
|
+
for (const id of source.sessionIds)
|
|
562
|
+
if (!wanted.has(id))
|
|
563
|
+
out.add(id);
|
|
564
|
+
}
|
|
565
|
+
return out;
|
|
566
|
+
})();
|
|
531
567
|
const adopted = [];
|
|
568
|
+
// Of `adopted`, the ones where this call PLACED a file — the only ones undo
|
|
569
|
+
// may ever delete. Computed here, where `fromWorkingDir` is still known;
|
|
570
|
+
// afterwards nothing can tell a copy from an in-place adoption.
|
|
571
|
+
const copies = [];
|
|
532
572
|
const skipped = [];
|
|
533
573
|
for (const fromWorkingDir of sources) {
|
|
534
574
|
// Sequential, not parallel: each source re-scans the adoption store, so a
|
|
@@ -542,8 +582,20 @@ export class HerdctlService {
|
|
|
542
582
|
skipped.push(...result.skipped);
|
|
543
583
|
for (const sessionId of result.adopted) {
|
|
544
584
|
const reason = withheld.get(sessionId);
|
|
585
|
+
// Not chosen: release it, and say nothing about it. Checked BEFORE the
|
|
586
|
+
// noise filter so a deselected session stays silent even if it would
|
|
587
|
+
// also have been withheld.
|
|
588
|
+
if (deselected?.has(sessionId) === true) {
|
|
589
|
+
if (!dryRun) {
|
|
590
|
+
await this.releaseWithheld(agent, project, fromWorkingDir, sessionId, mode);
|
|
591
|
+
}
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
545
594
|
if (reason === undefined) {
|
|
546
595
|
adopted.push(sessionId);
|
|
596
|
+
if (mode === "copy" && !dryRun && this.placedACopy(project, fromWorkingDir, sessionId)) {
|
|
597
|
+
copies.push(sessionId);
|
|
598
|
+
}
|
|
547
599
|
continue;
|
|
548
600
|
}
|
|
549
601
|
skipped.push({ sessionId, reason });
|
|
@@ -558,6 +610,8 @@ export class HerdctlService {
|
|
|
558
610
|
this.invalidateSessions(agent);
|
|
559
611
|
this.jobsIndexOrNull?.invalidate();
|
|
560
612
|
this.invalidateAdoptable(project.slug);
|
|
613
|
+
// Replaces any earlier import: only the most recent one is undoable.
|
|
614
|
+
this.lastImport.set(project.slug, { adopted: [...adopted], copies });
|
|
561
615
|
}
|
|
562
616
|
// A session adopted from one source and then met again in the NEXT source
|
|
563
617
|
// (its fresh copy now sits in the project's own transcript folder) comes back
|
|
@@ -567,6 +621,81 @@ export class HerdctlService {
|
|
|
567
621
|
const done = new Set(adopted);
|
|
568
622
|
return { adopted, skipped: skipped.filter((s) => !done.has(s.sessionId)) };
|
|
569
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Did an import of `sessionId` from `fromWorkingDir` place a NEW file?
|
|
626
|
+
*
|
|
627
|
+
* The same test `releaseWithheld` makes before deleting anything: an in-place
|
|
628
|
+
* adoption resolves the destination and the origin to one path, so there is no
|
|
629
|
+
* copy and nothing that may be removed.
|
|
630
|
+
*/
|
|
631
|
+
placedACopy(project, fromWorkingDir, sessionId) {
|
|
632
|
+
const home = this.cfg.claudeHome;
|
|
633
|
+
const placed = getCliSessionFile(project.workingDir, sessionId, home);
|
|
634
|
+
const origin = getCliSessionFile(fromWorkingDir, sessionId, home);
|
|
635
|
+
return path.resolve(placed) !== path.resolve(origin);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* What the LAST import into each workspace did, so it can be undone (#660).
|
|
639
|
+
*
|
|
640
|
+
* Keyed by workspace key (which may be `""` — the root). Holds the sessions
|
|
641
|
+
* that import actually brought in, and of those, the ones where it PLACED a
|
|
642
|
+
* copy and may therefore delete one again.
|
|
643
|
+
*
|
|
644
|
+
* Server-side rather than a token the client hands back: undo deletes files, so
|
|
645
|
+
* the set of paths it may touch must come from what this process actually did,
|
|
646
|
+
* not from a request body. A restart forgets it, which is the right failure —
|
|
647
|
+
* the offer to undo is a transient toast, not a durable feature.
|
|
648
|
+
*
|
|
649
|
+
* Only the most recent import per workspace is kept. Undoing an import two
|
|
650
|
+
* imports ago is not offered and would be a lie to describe as "undo".
|
|
651
|
+
*/
|
|
652
|
+
lastImport = new Map();
|
|
653
|
+
/**
|
|
654
|
+
* Undo the import this process most recently performed into `project` (#660).
|
|
655
|
+
*
|
|
656
|
+
* Releases each adoption and deletes the copies that import placed, leaving the
|
|
657
|
+
* user's own `~/.claude` history exactly as it was. Returns the sessions it
|
|
658
|
+
* released; an empty array means there was nothing to undo (nothing imported
|
|
659
|
+
* yet, already undone, or the server restarted since).
|
|
660
|
+
*
|
|
661
|
+
* `sessionIds` narrows it to part of that import — the rest stays. Ids that
|
|
662
|
+
* were not part of it are ignored rather than rejected: undo is a best-effort
|
|
663
|
+
* safety net, and the alternative is a 400 for a button the user pressed once.
|
|
664
|
+
*/
|
|
665
|
+
async unadoptChats(project, opts = {}) {
|
|
666
|
+
const agent = keeperAgentName(project.slug);
|
|
667
|
+
const last = this.lastImport.get(project.slug);
|
|
668
|
+
if (last === undefined)
|
|
669
|
+
return { released: [] };
|
|
670
|
+
const wanted = opts.sessionIds === undefined ? null : new Set(opts.sessionIds);
|
|
671
|
+
const target = last.adopted.filter((id) => wanted === null || wanted.has(id));
|
|
672
|
+
if (target.length === 0)
|
|
673
|
+
return { released: [] };
|
|
674
|
+
const copies = new Set(last.copies);
|
|
675
|
+
const released = [];
|
|
676
|
+
for (const sessionId of target) {
|
|
677
|
+
await this.manager.unadoptSession(agent, sessionId).catch(() => undefined);
|
|
678
|
+
// Delete ONLY a file this import placed. A session adopted in place has no
|
|
679
|
+
// copy, and its transcript is the user's own — removing it would destroy
|
|
680
|
+
// history rather than restore it.
|
|
681
|
+
if (copies.has(sessionId)) {
|
|
682
|
+
const placed = getCliSessionFile(project.workingDir, sessionId, this.cfg.claudeHome);
|
|
683
|
+
await fs.rm(placed, { force: true }).catch(() => undefined);
|
|
684
|
+
}
|
|
685
|
+
released.push(sessionId);
|
|
686
|
+
}
|
|
687
|
+
// Consume what was undone, so a second undo is a no-op rather than a repeat
|
|
688
|
+
// attempt to delete files that are already gone.
|
|
689
|
+
const remaining = last.adopted.filter((id) => !released.includes(id));
|
|
690
|
+
if (remaining.length === 0)
|
|
691
|
+
this.lastImport.delete(project.slug);
|
|
692
|
+
else
|
|
693
|
+
this.lastImport.set(project.slug, { adopted: remaining, copies: last.copies });
|
|
694
|
+
this.invalidateSessions(agent);
|
|
695
|
+
this.jobsIndexOrNull?.invalidate();
|
|
696
|
+
this.invalidateAdoptable(project.slug);
|
|
697
|
+
return { released };
|
|
698
|
+
}
|
|
570
699
|
/**
|
|
571
700
|
* Undo the adoption of a session detection had withheld, restoring the state
|
|
572
701
|
* that existed before this import. See {@link adoptChats} for why the file
|
|
@@ -877,11 +1006,28 @@ export class HerdctlService {
|
|
|
877
1006
|
}
|
|
878
1007
|
return { sessionId };
|
|
879
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Register a background-phase turn so Stop can reach it (paddock#528). Called
|
|
1011
|
+
* by the background turn sink once it knows both the synthetic job id it
|
|
1012
|
+
* published and the session that stream belongs to. Re-registering the same
|
|
1013
|
+
* job id with a later session id is fine — the sink refreshes the mapping if
|
|
1014
|
+
* the stream resolves a different session.
|
|
1015
|
+
*/
|
|
1016
|
+
registerBackgroundTurn(jobId, sessionId) {
|
|
1017
|
+
this.backgroundTurns.set(jobId, sessionId);
|
|
1018
|
+
}
|
|
1019
|
+
/** Forget a background-phase turn once its stream has ended. */
|
|
1020
|
+
unregisterBackgroundTurn(jobId) {
|
|
1021
|
+
this.backgroundTurns.delete(jobId);
|
|
1022
|
+
}
|
|
880
1023
|
/**
|
|
881
1024
|
* Cancel a running turn (Stop button → WS chat:cancel). Handles BOTH drive
|
|
882
1025
|
* modes off the single id the client holds as `jobId`:
|
|
883
1026
|
* - **session mode** — the id is a synthetic turn id in {@link liveSessions};
|
|
884
1027
|
* interrupt the live `RuntimeSession` (there is no herdctl job to cancel).
|
|
1028
|
+
* - **background phase** — the id is a synthetic id in
|
|
1029
|
+
* {@link backgroundTurns}; there is no model turn to interrupt, so reap the
|
|
1030
|
+
* session (paddock#528).
|
|
885
1031
|
* - **batch mode** — the id is a real herdctl job id; abort it via `cancelJob`
|
|
886
1032
|
* (which kills the CLI subprocess / aborts the SDK query).
|
|
887
1033
|
*
|
|
@@ -901,6 +1047,19 @@ export class HerdctlService {
|
|
|
901
1047
|
return false;
|
|
902
1048
|
}
|
|
903
1049
|
}
|
|
1050
|
+
// Background phase: the primary turn is over and its liveSessions entry is
|
|
1051
|
+
// gone, but the session is still open streaming autonomous re-invocation
|
|
1052
|
+
// turns. Stop here means "end this session" — reaping reaches even a session
|
|
1053
|
+
// the reap policy would hold open forever, and ending the stream is what
|
|
1054
|
+
// drives the sink's onDone → chat:complete → the UI unlocking.
|
|
1055
|
+
const backgroundSession = this.backgroundTurns.get(jobId);
|
|
1056
|
+
if (backgroundSession !== undefined) {
|
|
1057
|
+
const reaped = this.manager.reapChatSession(backgroundSession);
|
|
1058
|
+
if (!reaped) {
|
|
1059
|
+
console.warn(`[herdctl] no live managed session ${backgroundSession} to reap for background turn ${jobId}`);
|
|
1060
|
+
}
|
|
1061
|
+
return reaped;
|
|
1062
|
+
}
|
|
904
1063
|
try {
|
|
905
1064
|
await this.manager.cancelJob(jobId);
|
|
906
1065
|
return true;
|
|
@@ -1311,7 +1311,7 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
1311
1311
|
schema: {
|
|
1312
1312
|
tags: ["Chats"],
|
|
1313
1313
|
summary: "Count the native Claude Code chats this project could import",
|
|
1314
|
-
description: "Returns the terminal `claude` sessions that could be imported into this project but are not yet visible in it: the project's own working directory, plus any Claude transcript folder whose recorded working directory matches the project (by checkout name for a repo-backed project, by exact path for a notebook project). Sidechains, already-imported sessions and sessions belonging to a real Paddock run are excluded by the engine; empty
|
|
1314
|
+
description: "Returns the terminal `claude` sessions that could be imported into this project but are not yet visible in it: the project's own working directory, plus any Claude transcript folder whose recorded working directory matches the project (by checkout name for a repo-backed project, by exact path for a notebook project). Sidechains, already-imported sessions and sessions belonging to a real Paddock run are excluded by the engine; empty transcripts, slash-command-only transcripts and Paddock's own sweeper curation runs (`too-small` / `slash-command-only` / `sweeper-run`) are additionally withheld as noise and reported under `filtered`, so a count lower than the raw total always has an explanation. Response: `{ count, sources: [{ sourceCwd, sessionIds }], filtered: [{ sessionId, sourceCwd, reason }] }`. The count is LIVE — re-read it after an import rather than remembering a dismissal.",
|
|
1315
1315
|
params: {
|
|
1316
1316
|
type: "object",
|
|
1317
1317
|
properties: {
|
|
@@ -1340,7 +1340,7 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
1340
1340
|
schema: {
|
|
1341
1341
|
tags: ["Chats"],
|
|
1342
1342
|
summary: "Import native Claude Code chats into this project",
|
|
1343
|
-
description: "Imports the terminal `claude` sessions reported by GET …/adoptable-chats. With no body (or no `sourceCwd`) every detected source is imported; with `sourceCwd` only that source is, and it must be one this project actually offers — an unrecognised path is a 400 rather than an invitation to scan arbitrary directories. Transcripts are COPIED, never moved: the user's own `~/.claude` history is left intact and the copies keep their original mtimes, so imported chats sort by when they really happened. Afterwards the session, run-record and detection caches are dropped, so the chats appear on the very next list request with no restart. Response: `{ adopted, skipped }` — `skipped` carries the engine's reason per session (`sidechain`, `already-adopted`, `destination-exists`, `attributed-to-run`, `unreadable`, `placement-failed`, `record-failed`).",
|
|
1343
|
+
description: "Imports the terminal `claude` sessions reported by GET …/adoptable-chats. With no body (or no `sourceCwd`) every detected source is imported; with `sourceCwd` only that source is, and it must be one this project actually offers — an unrecognised path is a 400 rather than an invitation to scan arbitrary directories. `sessionIds` narrows the import to a chosen SUBSET of what is on offer (what the confirmation dialog sends); ids that are not on offer are simply not imported, and anything the engine adopts as a side effect of taking a whole source is released again. Transcripts are COPIED, never moved: the user's own `~/.claude` history is left intact and the copies keep their original mtimes, so imported chats sort by when they really happened. Afterwards the session, run-record and detection caches are dropped, so the chats appear on the very next list request with no restart. Response: `{ adopted, skipped }` — `skipped` carries the engine's reason per session (`sidechain`, `already-adopted`, `destination-exists`, `attributed-to-run`, `unreadable`, `placement-failed`, `record-failed`).",
|
|
1344
1344
|
params: {
|
|
1345
1345
|
type: "object",
|
|
1346
1346
|
properties: {
|
|
@@ -1355,6 +1355,11 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
1355
1355
|
sourceCwd: {
|
|
1356
1356
|
description: "Optional single source working directory to import from. Must be one of the `sourceCwd` values reported by GET …/adoptable-chats (or the project's own working directory); anything else is rejected with 400.",
|
|
1357
1357
|
},
|
|
1358
|
+
sessionIds: {
|
|
1359
|
+
type: "array",
|
|
1360
|
+
items: { type: "string" },
|
|
1361
|
+
description: "Optional subset of the offered session ids to import. Omit to import everything on offer.",
|
|
1362
|
+
},
|
|
1358
1363
|
},
|
|
1359
1364
|
required: [],
|
|
1360
1365
|
},
|
|
@@ -1382,7 +1387,10 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
1382
1387
|
.send({ error: `Not an importable source: ${sourceCwd}`, code: "invalid" });
|
|
1383
1388
|
}
|
|
1384
1389
|
}
|
|
1385
|
-
const result = await herdctl.adoptChats(project, {
|
|
1390
|
+
const result = await herdctl.adoptChats(project, {
|
|
1391
|
+
sourceCwd,
|
|
1392
|
+
sessionIds: req.body?.sessionIds,
|
|
1393
|
+
});
|
|
1386
1394
|
// Stamp each imported chat's provenance so the list can badge it as
|
|
1387
1395
|
// imported rather than as an ordinary human chat. `stampIfAbsent`, not
|
|
1388
1396
|
// `stamp`: provenance describes how a chat came to exist and is never
|
|
@@ -1403,4 +1411,52 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
1403
1411
|
return sendProjectError(reply, err);
|
|
1404
1412
|
}
|
|
1405
1413
|
});
|
|
1414
|
+
app.post("/unadopt-chats", {
|
|
1415
|
+
schema: {
|
|
1416
|
+
tags: ["Chats"],
|
|
1417
|
+
summary: "Undo the most recent native-chat import into this project",
|
|
1418
|
+
description: "Reverses the import this server most recently performed into the workspace: each adoption is released and the copies that import placed are deleted, leaving the user's own `~/.claude` history untouched. `sessionIds` undoes part of that import; omit it to undo all of it. Deliberately scoped to the LAST import and remembered in memory — undo may delete files, so the paths it can touch come from what this process actually did rather than from the request body, and a restart forgets the offer rather than acting on a stale one. Response: `{ released }`, the session ids actually released; an empty array means there was nothing to undo (nothing imported, already undone, or the server restarted since).",
|
|
1419
|
+
params: {
|
|
1420
|
+
type: "object",
|
|
1421
|
+
properties: {
|
|
1422
|
+
slug: { type: "string", description: "Project slug." },
|
|
1423
|
+
},
|
|
1424
|
+
required: ["slug"],
|
|
1425
|
+
},
|
|
1426
|
+
body: {
|
|
1427
|
+
type: ["object", "null"],
|
|
1428
|
+
additionalProperties: true,
|
|
1429
|
+
properties: {
|
|
1430
|
+
sessionIds: {
|
|
1431
|
+
type: "array",
|
|
1432
|
+
items: { type: "string" },
|
|
1433
|
+
description: "Optional subset of the last import's session ids to release. Omit to undo the whole import. Ids outside that import are ignored.",
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
required: [],
|
|
1437
|
+
},
|
|
1438
|
+
response: {
|
|
1439
|
+
200: {
|
|
1440
|
+
description: "Object `{ released }` listing the session ids released.",
|
|
1441
|
+
type: "object",
|
|
1442
|
+
additionalProperties: true,
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
}, async (req, reply) => {
|
|
1447
|
+
try {
|
|
1448
|
+
const project = await projects.get(req.params.slug);
|
|
1449
|
+
const result = await herdctl.unadoptChats(project, {
|
|
1450
|
+
sessionIds: req.body?.sessionIds,
|
|
1451
|
+
});
|
|
1452
|
+
// Drop the provenance marker the import stamped, so a session that is
|
|
1453
|
+
// later re-imported is badged by that import rather than carrying a
|
|
1454
|
+
// marker from one that has been undone.
|
|
1455
|
+
await Promise.all(result.released.map((sessionId) => runProvenance.clear(sessionId).catch(() => undefined)));
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
return sendProjectError(reply, err);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1406
1462
|
}
|
|
@@ -104,6 +104,17 @@ export declare class RunProvenanceStore {
|
|
|
104
104
|
* survives.
|
|
105
105
|
*/
|
|
106
106
|
stampIfAbsent(sessionId: string, provenance: RunProvenance): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Forget a chat's provenance (#660).
|
|
109
|
+
*
|
|
110
|
+
* The inverse of {@link stamp}, for the one case where a chat's creation is
|
|
111
|
+
* itself undone: undoing a native-chat import releases the adoption and deletes
|
|
112
|
+
* the copy, and a marker left behind would badge a LATER import of the same
|
|
113
|
+
* session as belonging to the one that was undone. Returns silently when there
|
|
114
|
+
* was nothing recorded, so an undo of a partially-stamped import is not an
|
|
115
|
+
* error.
|
|
116
|
+
*/
|
|
117
|
+
clear(sessionId: string): Promise<void>;
|
|
107
118
|
/** Write-through, serialised so overlapping stamps can't corrupt the file. */
|
|
108
119
|
private persist;
|
|
109
120
|
}
|
|
@@ -192,6 +192,24 @@ export class RunProvenanceStore {
|
|
|
192
192
|
return;
|
|
193
193
|
await this.stamp(sessionId, provenance);
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Forget a chat's provenance (#660).
|
|
197
|
+
*
|
|
198
|
+
* The inverse of {@link stamp}, for the one case where a chat's creation is
|
|
199
|
+
* itself undone: undoing a native-chat import releases the adoption and deletes
|
|
200
|
+
* the copy, and a marker left behind would badge a LATER import of the same
|
|
201
|
+
* session as belonging to the one that was undone. Returns silently when there
|
|
202
|
+
* was nothing recorded, so an undo of a partially-stamped import is not an
|
|
203
|
+
* error.
|
|
204
|
+
*/
|
|
205
|
+
async clear(sessionId) {
|
|
206
|
+
if (!isSafeId(sessionId))
|
|
207
|
+
return;
|
|
208
|
+
const map = await this.ensureLoaded();
|
|
209
|
+
if (!map.delete(sessionId))
|
|
210
|
+
return;
|
|
211
|
+
await this.persist(map);
|
|
212
|
+
}
|
|
195
213
|
/** Write-through, serialised so overlapping stamps can't corrupt the file. */
|
|
196
214
|
persist(map) {
|
|
197
215
|
this.writing = this.writing.then(async () => {
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The turn-execution engine, extracted from ws.ts (#403).
|
|
3
|
-
*
|
|
4
|
-
* `makeTurnEngine({ deps, hub })` relocates — intact — the mutually-recursive core
|
|
5
|
-
* that ws.ts's makeChatHandler used to hold in one closure: the trigger cluster,
|
|
6
|
-
* the self-MCP injection context, the per-turn injected-MCP builders + wake cache,
|
|
7
|
-
* the shared `startAgentTurn` execution engine, and the Layer-2/3 recovery
|
|
8
|
-
* (`injectRecoveryNudge` + `RecoveryEngine`). These pieces cross-reference each
|
|
9
|
-
* other (startAgentTurn ↔ recoveryEngine.armWatch, triggers ↔ startAgentTurn,
|
|
10
|
-
* selfMcp injection ↔ startAgentTurn), so they must share ONE scope — moving them
|
|
11
|
-
* as a group keeps that intact with no late-binding. ws.ts's socket layer consumes
|
|
12
|
-
* the returned surface.
|
|
13
|
-
*/
|
|
14
1
|
import type { SDKMessage, InjectedMcpServerDef } from "@herdctl/core";
|
|
15
2
|
import type { Project } from "./projects.js";
|
|
16
3
|
import { SessionHub } from "./session-hub.js";
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn-execution engine, extracted from ws.ts (#403).
|
|
3
|
+
*
|
|
4
|
+
* `makeTurnEngine({ deps, hub })` relocates — intact — the mutually-recursive core
|
|
5
|
+
* that ws.ts's makeChatHandler used to hold in one closure: the trigger cluster,
|
|
6
|
+
* the self-MCP injection context, the per-turn injected-MCP builders + wake cache,
|
|
7
|
+
* the shared `startAgentTurn` execution engine, and the Layer-2/3 recovery
|
|
8
|
+
* (`injectRecoveryNudge` + `RecoveryEngine`). These pieces cross-reference each
|
|
9
|
+
* other (startAgentTurn ↔ recoveryEngine.armWatch, triggers ↔ startAgentTurn,
|
|
10
|
+
* selfMcp injection ↔ startAgentTurn), so they must share ONE scope — moving them
|
|
11
|
+
* as a group keeps that intact with no late-binding. ws.ts's socket layer consumes
|
|
12
|
+
* the returned surface.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
1
15
|
import { createSDKMessageHandler } from "@herdctl/chat";
|
|
2
16
|
import { keeperAgentName, keeperSlugFromAgent } from "./herdctl.js";
|
|
3
17
|
import { isKnownDriveMode, getContextLimit } from "./models.js";
|
|
@@ -172,6 +186,13 @@ export function makeTurnEngine(engine) {
|
|
|
172
186
|
let producedReply = false;
|
|
173
187
|
let noticeEmitted = false;
|
|
174
188
|
let sawError = false;
|
|
189
|
+
// #528: the synthetic job id published for this background stretch, and the
|
|
190
|
+
// session id currently registered against it. The stretch is a real running
|
|
191
|
+
// turn as far as the UI is concerned, so it needs a cancellable identity —
|
|
192
|
+
// without one `chat:active` carries `jobId: null`, the client's deferred
|
|
193
|
+
// cancel never fires, and Stop puts nothing on the wire at all.
|
|
194
|
+
let bgJobId = null;
|
|
195
|
+
let registeredSession = null;
|
|
175
196
|
// #429: sub-agent launches recovered live from the tool_use input, keyed by
|
|
176
197
|
// toolUseId, so the enriched card renders without a refresh (see subagentLaunchFields).
|
|
177
198
|
const launches = new Map();
|
|
@@ -180,6 +201,19 @@ export function makeTurnEngine(engine) {
|
|
|
180
201
|
sessionId: resolvedSession,
|
|
181
202
|
jobId: turn?.jobId ?? null,
|
|
182
203
|
});
|
|
204
|
+
/**
|
|
205
|
+
* Point the published job id at whatever session this stream has resolved.
|
|
206
|
+
* Deferred rather than done once at mint time because the id is published when
|
|
207
|
+
* the turn opens, which can precede the first message that names a session;
|
|
208
|
+
* re-run on every message so a stream that resolves (or moves to) a different
|
|
209
|
+
* session stays cancellable.
|
|
210
|
+
*/
|
|
211
|
+
const syncBackgroundRegistration = () => {
|
|
212
|
+
if (!bgJobId || !resolvedSession || registeredSession === resolvedSession)
|
|
213
|
+
return;
|
|
214
|
+
deps.herdctl.registerBackgroundTurn(bgJobId, resolvedSession);
|
|
215
|
+
registeredSession = resolvedSession;
|
|
216
|
+
};
|
|
183
217
|
const emitNotice = (notice) => {
|
|
184
218
|
if (!turn || noticeEmitted)
|
|
185
219
|
return;
|
|
@@ -196,6 +230,13 @@ export function makeTurnEngine(engine) {
|
|
|
196
230
|
return;
|
|
197
231
|
resolvedSession = sid;
|
|
198
232
|
turn = hub.startTurn(projectSlug, null, sid);
|
|
233
|
+
// #528: give the stretch a cancellable identity the moment it opens, exactly
|
|
234
|
+
// as the foreground path does via onJobCreated. Stop then routes to a
|
|
235
|
+
// force-reap of this session rather than an interrupt — there is no model
|
|
236
|
+
// turn in flight here to interrupt.
|
|
237
|
+
bgJobId = randomUUID();
|
|
238
|
+
turn.setJobId(bgJobId);
|
|
239
|
+
syncBackgroundRegistration();
|
|
199
240
|
const t = turn;
|
|
200
241
|
translate = createSDKMessageHandler({
|
|
201
242
|
onText: (chunk) => {
|
|
@@ -252,6 +293,7 @@ export function makeTurnEngine(engine) {
|
|
|
252
293
|
ensureTurn(m.session_id ?? resolvedSession);
|
|
253
294
|
if (m.session_id)
|
|
254
295
|
turn.setSession(m.session_id);
|
|
296
|
+
syncBackgroundRegistration();
|
|
255
297
|
if (messageProducedReply(m))
|
|
256
298
|
producedReply = true;
|
|
257
299
|
const notice = noticeFromMessage(m);
|
|
@@ -266,6 +308,14 @@ export function makeTurnEngine(engine) {
|
|
|
266
308
|
// Finalize the single turn once the background stream ends (reaper reap). No-op
|
|
267
309
|
// if nothing was ever rendered (a sidechain-only stretch opened no turn).
|
|
268
310
|
const onDone = () => {
|
|
311
|
+
// Drop the cancellable identity first, and unconditionally: the stream is
|
|
312
|
+
// over, so a later Stop must fall through to the "already finished" path
|
|
313
|
+
// rather than reaping whatever session has since taken this id. Runs even
|
|
314
|
+
// for a sidechain-only stretch that opened no turn, and on a second call.
|
|
315
|
+
if (bgJobId)
|
|
316
|
+
deps.herdctl.unregisterBackgroundTurn(bgJobId);
|
|
317
|
+
bgJobId = null;
|
|
318
|
+
registeredSession = null;
|
|
269
319
|
if (!turn)
|
|
270
320
|
return;
|
|
271
321
|
turn.emit({
|
package/packages/web/dist/assets/{InstanceConfigPage-Cli_WHsM.js → InstanceConfigPage-xn9Rvoah.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-
|
|
1
|
+
import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-DtRblEY_.js";function $(){const[t,s]=o.useState(null),[l,i]=o.useState(!0),[r,c]=o.useState(null),[d,m]=o.useState({}),[h,k]=o.useState(!1),[j,p]=o.useState(null),[N,u]=o.useState(!1);o.useEffect(()=>{let a=!0;return i(!0),f.getInstanceConfig().then(n=>{a&&(s(n),c(null))}).catch(n=>a&&c(n instanceof Error?n.message:String(n))).finally(()=>a&&i(!1)),()=>{a=!1}},[]);const v=(a,n)=>{u(!1),p(null),m(g=>({...g,[a]:n}))},S=a=>Object.prototype.hasOwnProperty.call(d,a.key)?d[a.key]:a.value,y=o.useMemo(()=>t?t.groups.flatMap(a=>a.fields):[],[t]),x=o.useMemo(()=>y.filter(a=>a.editable&&!a.envOverridden).filter(a=>Object.prototype.hasOwnProperty.call(d,a.key)).filter(a=>!B(d[a.key],a.value)).map(a=>a.key),[y,d]),w=async()=>{if(x.length===0)return;const a={};for(const n of x)a[n]=d[n];k(!0),p(null);try{await f.updateInstanceConfig(a);const n=await f.getInstanceConfig();s(n),m({}),u(!0)}catch(n){p(n instanceof A||n instanceof Error?n.message:String(n))}finally{k(!1)}},C=()=>{m({}),p(null),u(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:"mx-auto max-w-2xl",children:[e.jsx(L,{saved:N,configPath:t==null?void 0:t.configPath}),l&&e.jsx("p",{className:"text-sm text-paddock-500",children:"Loading…"}),r&&e.jsxs("p",{className:"text-sm text-red-600 dark:text-red-400",role:"alert",children:["Failed to load settings: ",r]}),t&&t.groups.map(a=>e.jsx(M,{title:a.label,description:a.description,children:e.jsx("div",{className:"grid grid-cols-1 gap-4",children:a.fields.map(n=>e.jsx(P,{field:n,value:S(n),onChange:g=>v(n.key,g)},n.key))})},a.id))]})}),t&&e.jsxs("footer",{className:"flex items-center gap-3 border-t border-paddock-200 px-4 py-3 dark:border-paddock-800 sm:px-6",children:[j&&e.jsxs("span",{className:"flex items-center gap-1.5 text-[13px] text-red-600 dark:text-red-400",role:"alert",children:[e.jsx(b,{width:14,height:14,className:"shrink-0"}),j]}),e.jsx("span",{className:"ml-auto text-[12px] text-paddock-400",children:x.length>0?`${x.length} unsaved change${x.length===1?"":"s"}`:"No changes"}),e.jsx("button",{type:"button",className:"btn-subtle",onClick:C,disabled:x.length===0||h,children:"Reset"}),e.jsx("button",{type:"button",className:"btn-primary",onClick:w,disabled:x.length===0||h,children:h?"Saving…":"Save changes"})]})]})}function L({saved:t,configPath:s}){return e.jsxs("div",{role:"status",className:`mb-6 flex items-start gap-2 rounded-lg border px-3 py-2.5 text-[13px] leading-snug ${t?"border-emerald-300 bg-emerald-50 text-emerald-800 dark:border-emerald-700/60 dark:bg-emerald-900/20 dark:text-emerald-300":"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300"}`,children:[t?e.jsx(E,{width:15,height:15,className:"mt-0.5 shrink-0"}):e.jsx(b,{width:15,height:15,className:"mt-0.5 shrink-0"}),e.jsx("span",{children:t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):e.jsxs(e.Fragment,{children:["Changes here are written to ",e.jsx("code",{className:"font-mono text-[12px]",children:T(s)})," ","and take effect only after the server restarts — the running instance keeps its current config until then."]})})]})}function M({title:t,description:s,children:l}){return e.jsxs("section",{className:"mb-6",children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wide text-paddock-500",children:t}),s&&e.jsx("p",{className:"mb-3 mt-0.5 text-[13px] text-paddock-500",children:s}),e.jsx("div",{className:`card ${s?"":"mt-2"}`,children:l})]})}function P({field:t,value:s,onChange:l}){const i=!t.editable||t.envOverridden,r=`cfg-${t.key}`;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("label",{htmlFor:i?void 0:r,className:"text-sm font-medium",children:[t.label,t.sensitive&&e.jsx("span",{className:"ml-1.5 align-middle text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400",children:"sensitive"})]}),t.type==="boolean"&&!i&&e.jsx("input",{id:r,type:"checkbox",className:"h-4 w-4 accent-accent",checked:!!s,onChange:c=>l(c.target.checked)})]}),t.help&&e.jsx("p",{className:"mt-0.5 text-[12px] leading-snug text-paddock-500",children:t.help}),i?e.jsx(V,{field:t,value:s}):t.type!=="boolean"&&e.jsx(R,{field:t,value:s,onChange:l,inputId:r}),t.envOverridden&&e.jsxs("p",{className:"mt-1 flex items-start gap-1.5 text-[12px] leading-snug text-amber-600 dark:text-amber-400",children:[e.jsx(b,{width:13,height:13,className:"mt-0.5 shrink-0"}),e.jsxs("span",{children:["Overridden by environment variable ",e.jsx("code",{className:"font-mono",children:t.envVar})," — edit that env var (and restart) to change it."]})]})]})}function R({field:t,value:s,onChange:l,inputId:i}){if(t.type==="enum")return e.jsx("select",{id:i,className:"input mt-1.5",value:String(s??""),onChange:r=>l(r.target.value),children:(t.enumValues??[]).map(r=>e.jsx("option",{value:r,children:r},r))});if(t.type==="number")return e.jsx("input",{id:i,type:"number",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"default":String(t.default),onChange:r=>l(r.target.value===""?null:Number(r.target.value))});if(t.type==="text"){const r=s==null||s===t.default,c=s==null?String(t.default??""):String(s),d=typeof t.default=="string"&&!r;return e.jsxs("div",{className:"mt-1.5",children:[e.jsx("textarea",{id:i,className:"input min-h-[9rem] resize-y font-mono text-[12px] leading-relaxed",rows:10,spellCheck:!1,value:c,onChange:m=>l(m.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-[12px] text-paddock-500",children:[e.jsx("span",{children:r?"Using the built-in default.":s===""?"Empty — nothing will be appended.":`${c.length.toLocaleString()} characters.`}),d&&e.jsx("button",{type:"button",className:"shrink-0 underline underline-offset-2 hover:text-paddock-700 dark:hover:text-paddock-300",onClick:()=>l(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const r=Array.isArray(s)?s.join(", "):String(s??"");return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:r,onChange:c=>l(c.target.value.split(",").map(d=>d.trim()).filter(Boolean))})}return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"":String(t.default),onChange:r=>l(r.target.value)})}function V({field:t,value:s}){return e.jsx("div",{className:"mt-1.5 rounded-md border border-dashed border-paddock-300 bg-paddock-50 px-2.5 py-1.5 text-[13px] text-paddock-600 dark:border-paddock-700 dark:bg-paddock-900/40 dark:text-paddock-300",children:t.type==="boolean"?e.jsx("span",{className:"font-mono",children:s?"true":"false"}):s==null||s===""?e.jsx("span",{className:"italic text-paddock-400",children:t.type==="text"&&s===""?"(empty — nothing appended)":"(not set)"}):t.type==="text"?e.jsx("span",{className:"block max-h-64 overflow-y-auto whitespace-pre-wrap font-mono text-[12px] leading-relaxed",children:String(s)}):e.jsx("span",{className:"break-all font-mono",children:Array.isArray(s)?s.join(", "):String(s)})})}function B(t,s){return Array.isArray(t)&&Array.isArray(s)?t.length===s.length&&t.every((l,i)=>l===s[i]):t===s}function T(t){if(!t)return"paddock.config.yaml";const s=t.split("/");return s[s.length-1]||t}function z(){const t=O(),s=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-paddock-200 px-3 pb-2.5 dark:border-paddock-800 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:s,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(F,{width:20,height:20})}),e.jsx(I,{width:18,height:18,className:"shrink-0 text-paddock-400"}),e.jsx("h1",{className:"text-[15px] font-semibold tracking-tight",children:"Config"})]}),e.jsx($,{})]})}export{z as InstanceConfigPage};
|
|
2
2
|
|