plotlink-ows 1.0.20 → 1.0.23
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/app/routes/settings.ts +12 -5
- package/app/routes/terminal.ts +35 -4
- package/app/web/components/PreviewPanel.tsx +1 -1
- package/app/web/components/StoriesPage.tsx +11 -3
- package/app/web/components/TerminalPanel.tsx +46 -1
- package/app/web/dist/assets/{index-vjSa2kh-.js → index-CU2rL-Z3.js} +38 -38
- package/app/web/dist/index.html +1 -1
- package/package.json +1 -1
package/app/routes/settings.ts
CHANGED
|
@@ -67,6 +67,9 @@ settings.post("/generate-binding", async (c) => {
|
|
|
67
67
|
agentName: (config.agentName as string) || undefined,
|
|
68
68
|
agentDescription: (config.agentDescription as string) || undefined,
|
|
69
69
|
agentGenre: (config.agentGenre as string) || undefined,
|
|
70
|
+
agentLlmModel: (config.agentLlmModel as string) || undefined,
|
|
71
|
+
agentRegisteredBy: (config.agentRegisteredBy as string) || undefined,
|
|
72
|
+
agentRegisteredAt: (config.agentRegisteredAt as string) || undefined,
|
|
70
73
|
});
|
|
71
74
|
} catch (err: unknown) {
|
|
72
75
|
const msg = err instanceof Error ? err.message : "Failed to generate binding proof";
|
|
@@ -107,13 +110,14 @@ settings.post("/register-agent", async (c) => {
|
|
|
107
110
|
} catch { /* not registered — continue */ }
|
|
108
111
|
|
|
109
112
|
// Build agentURI as inline JSON
|
|
113
|
+
const registeredAt = new Date().toISOString();
|
|
110
114
|
const agentURI = JSON.stringify({
|
|
111
115
|
name: body.name.trim(),
|
|
112
116
|
description: body.description.trim(),
|
|
113
117
|
...(body.genre?.trim() && { genre: body.genre.trim() }),
|
|
114
118
|
llmModel: "Claude",
|
|
115
119
|
registeredBy: "plotlink-ows",
|
|
116
|
-
registeredAt
|
|
120
|
+
registeredAt,
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
// Create OWS-backed wallet client and call register()
|
|
@@ -155,12 +159,15 @@ settings.post("/register-agent", async (c) => {
|
|
|
155
159
|
return c.json({ error: "Transaction succeeded but Registered event not found" }, 500);
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
// Cache
|
|
162
|
+
// Cache full tokenURI data in config.json (survives npx reinstalls, no Prisma dependency)
|
|
159
163
|
writeConfig({
|
|
160
164
|
agentId,
|
|
161
|
-
agentName: body.name,
|
|
162
|
-
agentDescription: body.description,
|
|
163
|
-
...(body.genre && { agentGenre: body.genre }),
|
|
165
|
+
agentName: body.name.trim(),
|
|
166
|
+
agentDescription: body.description.trim(),
|
|
167
|
+
...(body.genre?.trim() && { agentGenre: body.genre.trim() }),
|
|
168
|
+
agentLlmModel: "Claude",
|
|
169
|
+
agentRegisteredBy: "plotlink-ows",
|
|
170
|
+
agentRegisteredAt: registeredAt,
|
|
164
171
|
});
|
|
165
172
|
|
|
166
173
|
return c.json({
|
package/app/routes/terminal.ts
CHANGED
|
@@ -84,14 +84,19 @@ function spawnPty(storyName: string, opts?: { sessionId?: string; resume?: boole
|
|
|
84
84
|
ptySessions.set(storyName, session);
|
|
85
85
|
|
|
86
86
|
term.onExit(({ exitCode }) => {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Find this session by term reference — key may have changed via rename
|
|
88
|
+
let currentName: string | undefined;
|
|
89
|
+
let s: typeof session | undefined;
|
|
90
|
+
for (const [key, entry] of ptySessions) {
|
|
91
|
+
if (entry.term === term) { currentName = key; s = entry; break; }
|
|
92
|
+
}
|
|
93
|
+
if (!currentName || !s) return;
|
|
89
94
|
|
|
90
95
|
// If a resumed session exits quickly (< 5s), signal client to auto-reconnect fresh
|
|
91
96
|
const elapsed = Date.now() - spawnTime;
|
|
92
97
|
if (isResume && elapsed < 5000 && exitCode !== 0) {
|
|
93
|
-
console.log(`Resume for "${
|
|
94
|
-
ptySessions.delete(
|
|
98
|
+
console.log(`Resume for "${currentName}" failed (exit ${exitCode} in ${elapsed}ms), signaling fresh fallback`);
|
|
99
|
+
ptySessions.delete(currentName);
|
|
95
100
|
if (s.ws && s.ws.readyState <= 1) {
|
|
96
101
|
// Close code 4000 = resume-failed, client should auto-reconnect fresh
|
|
97
102
|
s.ws.close(4000, "resume-failed");
|
|
@@ -193,6 +198,32 @@ terminal.delete("/:storyName/discard", (c) => {
|
|
|
193
198
|
return c.json({ ok: true });
|
|
194
199
|
});
|
|
195
200
|
|
|
201
|
+
/** POST /api/terminal/rename — rename a session key without killing the process */
|
|
202
|
+
terminal.post("/rename", async (c) => {
|
|
203
|
+
const body = await c.req.json<{ oldName?: string; newName?: string }>().catch(() => ({}));
|
|
204
|
+
const oldName = body.oldName && safeName(body.oldName);
|
|
205
|
+
const newName = body.newName && safeName(body.newName);
|
|
206
|
+
if (!oldName || !newName) return c.json({ error: "Invalid names" }, 400);
|
|
207
|
+
if (oldName === newName) return c.json({ ok: true });
|
|
208
|
+
|
|
209
|
+
const session = ptySessions.get(oldName);
|
|
210
|
+
if (!session) return c.json({ error: "Session not found" }, 404);
|
|
211
|
+
|
|
212
|
+
if (ptySessions.has(newName)) return c.json({ error: "Target session already exists" }, 409);
|
|
213
|
+
|
|
214
|
+
// Move in-memory PTY entry
|
|
215
|
+
ptySessions.delete(oldName);
|
|
216
|
+
ptySessions.set(newName, session);
|
|
217
|
+
|
|
218
|
+
// Update persisted session map: remove old key, store under new key
|
|
219
|
+
const sessionMap = loadSessionMap();
|
|
220
|
+
delete sessionMap[oldName];
|
|
221
|
+
sessionMap[newName] = session.sessionId;
|
|
222
|
+
saveSessionMap(sessionMap);
|
|
223
|
+
|
|
224
|
+
return c.json({ ok: true, sessionId: session.sessionId });
|
|
225
|
+
});
|
|
226
|
+
|
|
196
227
|
/** POST /api/terminal/stop — kill PTY (legacy, kills default) */
|
|
197
228
|
terminal.post("/stop", (c) => {
|
|
198
229
|
const session = ptySessions.get("default");
|
|
@@ -150,7 +150,7 @@ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publis
|
|
|
150
150
|
const isGenesis = fileName === "genesis.md";
|
|
151
151
|
const isPlot = fileName ? /^plot-\d+\.md$/.test(fileName) : false;
|
|
152
152
|
const isPublished = fileData?.status === "published" || fileData?.status === "published-not-indexed";
|
|
153
|
-
const charLimit = isGenesis
|
|
153
|
+
const charLimit = (isGenesis || isPlot) ? 10000 : null;
|
|
154
154
|
// Don't show over-limit warning for already-published files
|
|
155
155
|
const overLimit = !isPublished && charLimit !== null && charCount > charLimit;
|
|
156
156
|
|
|
@@ -41,6 +41,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
|
|
|
41
41
|
const [ratio, setRatio] = useState(loadRatio);
|
|
42
42
|
const [untitledSessions, setUntitledSessions] = useState<string[]>([]);
|
|
43
43
|
const knownStoriesRef = useRef<Set<string>>(new Set());
|
|
44
|
+
const renameRef = useRef<((oldName: string, newName: string) => Promise<boolean>) | null>(null);
|
|
44
45
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
45
46
|
const dragging = useRef(false);
|
|
46
47
|
|
|
@@ -85,8 +86,15 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
|
|
|
85
86
|
// Detect newly appeared stories
|
|
86
87
|
for (const name of currentNames) {
|
|
87
88
|
if (!knownStoriesRef.current.has(name) && untitledSessions.length > 0) {
|
|
88
|
-
// New story appeared —
|
|
89
|
-
|
|
89
|
+
// New story appeared — rename the oldest untitled session to the story name
|
|
90
|
+
const oldName = untitledSessions[0];
|
|
91
|
+
let renamed = false;
|
|
92
|
+
if (renameRef.current) {
|
|
93
|
+
renamed = await renameRef.current(oldName, name).catch(() => false);
|
|
94
|
+
}
|
|
95
|
+
if (renamed) {
|
|
96
|
+
setUntitledSessions((prev) => prev.slice(1));
|
|
97
|
+
}
|
|
90
98
|
setSelectedStory(name);
|
|
91
99
|
setSelectedFile(null);
|
|
92
100
|
}
|
|
@@ -330,7 +338,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
|
|
|
330
338
|
|
|
331
339
|
{/* Terminal — sized by ratio of available space */}
|
|
332
340
|
<div className="min-w-0 border-r border-border" style={{ flex: `${ratio} 0 0` }}>
|
|
333
|
-
<TerminalPanel token={token} storyName={selectedStory} authFetch={authFetch} onSelectStory={handleSelectStory} onDestroySession={handleDestroySession} onArchiveStory={handleArchiveStory} confirmedStories={confirmedStories} />
|
|
341
|
+
<TerminalPanel token={token} storyName={selectedStory} authFetch={authFetch} onSelectStory={handleSelectStory} onDestroySession={handleDestroySession} onArchiveStory={handleArchiveStory} confirmedStories={confirmedStories} renameRef={renameRef} />
|
|
334
342
|
</div>
|
|
335
343
|
|
|
336
344
|
{/* Drag Handle */}
|
|
@@ -12,6 +12,7 @@ interface TerminalPanelProps {
|
|
|
12
12
|
onDestroySession?: (storyName: string) => void;
|
|
13
13
|
onArchiveStory?: (storyName: string) => void;
|
|
14
14
|
confirmedStories?: Set<string>;
|
|
15
|
+
renameRef?: React.RefObject<((oldName: string, newName: string) => Promise<boolean>) | null>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface TerminalSession {
|
|
@@ -105,7 +106,7 @@ async function deleteScrollback(storyName: string): Promise<void> {
|
|
|
105
106
|
// Sessions live outside React state to avoid ref-in-effect lint issues
|
|
106
107
|
const sessions = new Map<string, TerminalSession>();
|
|
107
108
|
|
|
108
|
-
export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories }: TerminalPanelProps) {
|
|
109
|
+
export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories, renameRef }: TerminalPanelProps) {
|
|
109
110
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
110
111
|
const authFetchRef = useRef(authFetch);
|
|
111
112
|
const [sessionList, setSessionList] = useState<string[]>([]);
|
|
@@ -327,6 +328,50 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
327
328
|
onDestroySession?.(name);
|
|
328
329
|
}, [authFetch, onDestroySession]);
|
|
329
330
|
|
|
331
|
+
/** Rename a session key (e.g. _new_123 → paper-chair) without killing the PTY.
|
|
332
|
+
* Returns true on success, false on failure. */
|
|
333
|
+
const renameSession = useCallback(async (oldName: string, newName: string): Promise<boolean> => {
|
|
334
|
+
const session = sessions.get(oldName);
|
|
335
|
+
if (!session || sessions.has(newName)) return false;
|
|
336
|
+
|
|
337
|
+
// Rename on the server first
|
|
338
|
+
const res = await authFetchRef.current("/api/terminal/rename", {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { "Content-Type": "application/json" },
|
|
341
|
+
body: JSON.stringify({ oldName, newName }),
|
|
342
|
+
});
|
|
343
|
+
if (!res.ok) return false;
|
|
344
|
+
|
|
345
|
+
// Move in client-side sessions map
|
|
346
|
+
sessions.delete(oldName);
|
|
347
|
+
sessions.set(newName, session);
|
|
348
|
+
|
|
349
|
+
// Migrate scrollback under the new key
|
|
350
|
+
try {
|
|
351
|
+
const data = session.serialize.serialize();
|
|
352
|
+
await deleteScrollback(oldName);
|
|
353
|
+
await saveScrollback(newName, data);
|
|
354
|
+
} catch { /* ignore */ }
|
|
355
|
+
|
|
356
|
+
// Update React state
|
|
357
|
+
setSessionList((prev) => prev.map((s) => (s === oldName ? newName : s)));
|
|
358
|
+
setDisconnected((prev) => {
|
|
359
|
+
if (!prev.has(oldName)) return prev;
|
|
360
|
+
const next = new Set(prev);
|
|
361
|
+
next.delete(oldName);
|
|
362
|
+
next.add(newName);
|
|
363
|
+
return next;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return true;
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
// Expose renameSession to parent via ref
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (renameRef) renameRef.current = renameSession;
|
|
372
|
+
return () => { if (renameRef) renameRef.current = null; };
|
|
373
|
+
}, [renameRef, renameSession]);
|
|
374
|
+
|
|
330
375
|
// Auto-spawn + show/hide when story changes
|
|
331
376
|
useEffect(() => {
|
|
332
377
|
if (!storyName) return;
|