plotlink-ows 1.0.19 → 1.0.22
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 +20 -6
- package/app/routes/terminal.ts +35 -4
- 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-MTuqiJdD.js} +38 -38
- package/app/web/dist/index.html +1 -1
- package/package.json +1 -1
package/app/routes/settings.ts
CHANGED
|
@@ -56,15 +56,20 @@ settings.post("/generate-binding", async (c) => {
|
|
|
56
56
|
const result = owsSignMsg(wallet.name, "eip155:8453", message, passphrase);
|
|
57
57
|
const signature = result.signature.startsWith("0x") ? result.signature : `0x${result.signature}`;
|
|
58
58
|
|
|
59
|
-
// Include
|
|
59
|
+
// Include agent data from config.json if available
|
|
60
60
|
const config = readConfig();
|
|
61
|
-
const agentId = config.agentId ? Number(config.agentId) : undefined;
|
|
62
61
|
|
|
63
62
|
return c.json({
|
|
64
63
|
message,
|
|
65
64
|
signature,
|
|
66
65
|
owsWallet,
|
|
67
|
-
agentId,
|
|
66
|
+
agentId: config.agentId ? Number(config.agentId) : undefined,
|
|
67
|
+
agentName: (config.agentName as string) || undefined,
|
|
68
|
+
agentDescription: (config.agentDescription as string) || undefined,
|
|
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,
|
|
68
73
|
});
|
|
69
74
|
} catch (err: unknown) {
|
|
70
75
|
const msg = err instanceof Error ? err.message : "Failed to generate binding proof";
|
|
@@ -105,13 +110,14 @@ settings.post("/register-agent", async (c) => {
|
|
|
105
110
|
} catch { /* not registered — continue */ }
|
|
106
111
|
|
|
107
112
|
// Build agentURI as inline JSON
|
|
113
|
+
const registeredAt = new Date().toISOString();
|
|
108
114
|
const agentURI = JSON.stringify({
|
|
109
115
|
name: body.name.trim(),
|
|
110
116
|
description: body.description.trim(),
|
|
111
117
|
...(body.genre?.trim() && { genre: body.genre.trim() }),
|
|
112
118
|
llmModel: "Claude",
|
|
113
119
|
registeredBy: "plotlink-ows",
|
|
114
|
-
registeredAt
|
|
120
|
+
registeredAt,
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
// Create OWS-backed wallet client and call register()
|
|
@@ -153,8 +159,16 @@ settings.post("/register-agent", async (c) => {
|
|
|
153
159
|
return c.json({ error: "Transaction succeeded but Registered event not found" }, 500);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
// Cache
|
|
157
|
-
writeConfig({
|
|
162
|
+
// Cache full tokenURI data in config.json (survives npx reinstalls, no Prisma dependency)
|
|
163
|
+
writeConfig({
|
|
164
|
+
agentId,
|
|
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,
|
|
171
|
+
});
|
|
158
172
|
|
|
159
173
|
return c.json({
|
|
160
174
|
agentId,
|
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");
|
|
@@ -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;
|