plotlink-ows 1.0.33 → 1.2.95
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/README.md +4 -0
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +813 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +8 -1
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +242 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +259 -45
- package/app/routes/settings.ts +92 -37
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/routes/wallet.ts +58 -30
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1337 -0
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1182 -0
- package/app/web/components/PreviewPanel.tsx +952 -78
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +745 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +446 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +156 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-DxATSk7X.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
|
@@ -3,6 +3,17 @@ import { Terminal } from "@xterm/xterm";
|
|
|
3
3
|
import { FitAddon } from "@xterm/addon-fit";
|
|
4
4
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
5
5
|
import "@xterm/xterm/css/xterm.css";
|
|
6
|
+
import { isCodexAuthUnclear, CODEX_AUTH_UNCLEAR_MESSAGE, type AgentReadiness } from "@app-lib/agent-readiness";
|
|
7
|
+
import { FRESH_SPAWN_SIGNAL } from "@app-lib/terminal-protocol";
|
|
8
|
+
import { redactTerminalSecrets } from "@app-lib/terminal-redact";
|
|
9
|
+
|
|
10
|
+
/** Story metadata persisted with a `_new_*` → real-folder rename (#295). */
|
|
11
|
+
export interface RenameMeta {
|
|
12
|
+
contentType?: "fiction" | "cartoon";
|
|
13
|
+
language?: string;
|
|
14
|
+
agentMode?: "normal" | "bypass";
|
|
15
|
+
agentProvider?: "claude" | "codex";
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
interface TerminalPanelProps {
|
|
8
19
|
token: string;
|
|
@@ -12,7 +23,49 @@ interface TerminalPanelProps {
|
|
|
12
23
|
onDestroySession?: (storyName: string) => void;
|
|
13
24
|
onArchiveStory?: (storyName: string) => void;
|
|
14
25
|
confirmedStories?: Set<string>;
|
|
15
|
-
|
|
26
|
+
// The optional `meta` is persisted to the confirmed story's .story.json
|
|
27
|
+
// atomically with the rename so a fresh story's provider/contentType survive (#295).
|
|
28
|
+
renameRef?: React.RefObject<((oldName: string, newName: string, meta?: RenameMeta) => Promise<boolean>) | null>;
|
|
29
|
+
bypassStories?: Record<string, boolean>;
|
|
30
|
+
agentProviders?: Record<string, "claude" | "codex">;
|
|
31
|
+
/** Local agent (Codex) readiness. null/undefined = not yet loaded (fail-open). */
|
|
32
|
+
readiness?: AgentReadiness | null;
|
|
33
|
+
/** Content type of the currently-selected story (undefined = unknown). */
|
|
34
|
+
contentType?: "fiction" | "cartoon";
|
|
35
|
+
/**
|
|
36
|
+
* True only for the selected real (non-`_new_*`) cartoon story whose
|
|
37
|
+
* `.story.json` has no `agentProvider` recorded (legacy). When true, show the
|
|
38
|
+
* explicit provider-repair CTA instead of auto-spawning, so the writer sets
|
|
39
|
+
* the provider to Codex before launching. Never true for fiction or a cartoon
|
|
40
|
+
* that already has a provider.
|
|
41
|
+
*/
|
|
42
|
+
needsProviderRepair?: boolean;
|
|
43
|
+
/** Set this story's provider to Codex (scoped, non-destructive repair). */
|
|
44
|
+
onRepairProvider?: () => void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CODEX_ENABLE_CMD = "codex features enable image_generation";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pure predicate: should the cartoon agent LAUNCH be blocked?
|
|
51
|
+
*
|
|
52
|
+
* Blocked ONLY when ALL of:
|
|
53
|
+
* - the selected story is a cartoon, AND
|
|
54
|
+
* - readiness has loaded (non-null), AND
|
|
55
|
+
* - Codex is NOT ready (not installed OR image generation not enabled).
|
|
56
|
+
*
|
|
57
|
+
* Fail-open: readiness null/undefined => NOT blocked (a probe failure must never
|
|
58
|
+
* brick terminals). Fiction / undefined contentType => NEVER blocked.
|
|
59
|
+
*/
|
|
60
|
+
export function isCartoonLaunchBlocked(
|
|
61
|
+
contentType: "fiction" | "cartoon" | undefined,
|
|
62
|
+
readiness: AgentReadiness | null | undefined,
|
|
63
|
+
): boolean {
|
|
64
|
+
if (contentType !== "cartoon") return false;
|
|
65
|
+
if (!readiness) return false; // fail-open until readiness resolves
|
|
66
|
+
const codexReady =
|
|
67
|
+
readiness.codex.installed && readiness.codex.imageGeneration === "enabled";
|
|
68
|
+
return !codexReady;
|
|
16
69
|
}
|
|
17
70
|
|
|
18
71
|
interface TerminalSession {
|
|
@@ -106,13 +159,21 @@ async function deleteScrollback(storyName: string): Promise<void> {
|
|
|
106
159
|
// Sessions live outside React state to avoid ref-in-effect lint issues
|
|
107
160
|
const sessions = new Map<string, TerminalSession>();
|
|
108
161
|
|
|
109
|
-
export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories, renameRef }: TerminalPanelProps) {
|
|
162
|
+
export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDestroySession, onArchiveStory, confirmedStories, renameRef, bypassStories, agentProviders, readiness, contentType, needsProviderRepair, onRepairProvider }: TerminalPanelProps) {
|
|
110
163
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
111
164
|
const authFetchRef = useRef(authFetch);
|
|
112
165
|
const [sessionList, setSessionList] = useState<string[]>([]);
|
|
113
166
|
const [disconnected, setDisconnected] = useState<Set<string>>(new Set());
|
|
114
167
|
const [confirmingDiscard, setConfirmingDiscard] = useState<string | null>(null);
|
|
115
168
|
const [confirmingArchive, setConfirmingArchive] = useState<string | null>(null);
|
|
169
|
+
const [copiedEnableCmd, setCopiedEnableCmd] = useState(false);
|
|
170
|
+
const [repairing, setRepairing] = useState(false);
|
|
171
|
+
|
|
172
|
+
// Gate the cartoon agent launch for the currently-selected story.
|
|
173
|
+
const cartoonLaunchBlocked = isCartoonLaunchBlocked(contentType, readiness);
|
|
174
|
+
// Legacy cartoon (no provider recorded) ⇒ require explicit provider repair
|
|
175
|
+
// before auto-spawning a terminal. Scoped to the selected story only.
|
|
176
|
+
const showProviderRepair = !!needsProviderRepair;
|
|
116
177
|
|
|
117
178
|
const connectWsRef = useRef<(name: string, session: TerminalSession, resume: boolean) => void>(() => {});
|
|
118
179
|
|
|
@@ -142,10 +203,19 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
142
203
|
}
|
|
143
204
|
}, [safeFit]);
|
|
144
205
|
|
|
206
|
+
const bypassRef = useRef<Record<string, boolean>>({});
|
|
207
|
+
useEffect(() => { bypassRef.current = bypassStories || {}; }, [bypassStories]);
|
|
208
|
+
|
|
209
|
+
const providerRef = useRef<Record<string, "claude" | "codex">>({});
|
|
210
|
+
useEffect(() => { providerRef.current = agentProviders || {}; }, [agentProviders]);
|
|
211
|
+
|
|
145
212
|
const connectWs = useCallback((name: string, session: TerminalSession, resume: boolean) => {
|
|
146
213
|
const wsProto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
214
|
+
const bypass = bypassRef.current[name] ? "&bypass=true" : "";
|
|
215
|
+
const provider = providerRef.current[name];
|
|
216
|
+
const providerParam = provider ? `&provider=${encodeURIComponent(provider)}` : "";
|
|
147
217
|
const ws = new WebSocket(
|
|
148
|
-
`${wsProto}//${window.location.host}/ws/terminal?story=${encodeURIComponent(name)}&token=${token}&resume=${resume}`
|
|
218
|
+
`${wsProto}//${window.location.host}/ws/terminal?story=${encodeURIComponent(name)}&token=${token}&resume=${resume}${bypass}${providerParam}`
|
|
149
219
|
);
|
|
150
220
|
|
|
151
221
|
ws.onopen = () => {
|
|
@@ -155,8 +225,22 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
155
225
|
ws.send(JSON.stringify({ type: "resize", cols: session.term.cols, rows: session.term.rows }));
|
|
156
226
|
};
|
|
157
227
|
|
|
228
|
+
// The very first frame may be a control signal (#453). A fresh server spawn
|
|
229
|
+
// (the agent process reprints its banner/history) sends FRESH_SPAWN_SIGNAL —
|
|
230
|
+
// drop the restored scrollback so the banner isn't duplicated. Any other
|
|
231
|
+
// first frame (a live-PTY reconnect) is normal PTY output and is kept.
|
|
232
|
+
let firstFrame = true;
|
|
158
233
|
ws.onmessage = (e) => {
|
|
159
|
-
|
|
234
|
+
if (firstFrame) {
|
|
235
|
+
firstFrame = false;
|
|
236
|
+
if (typeof e.data === "string" && e.data === FRESH_SPAWN_SIGNAL) {
|
|
237
|
+
session.term.reset();
|
|
238
|
+
deleteScrollback(name).catch(() => {});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Mask obvious auth secrets before they reach the terminal / scrollback (#454).
|
|
243
|
+
session.term.write(typeof e.data === "string" ? redactTerminalSecrets(e.data) : e.data);
|
|
160
244
|
};
|
|
161
245
|
|
|
162
246
|
ws.onclose = (event) => {
|
|
@@ -242,11 +326,16 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
242
326
|
sessions.set(name, session);
|
|
243
327
|
setSessionList((prev) => [...prev, name]);
|
|
244
328
|
|
|
245
|
-
// Restore scrollback from IndexedDB
|
|
329
|
+
// Restore scrollback from IndexedDB, masking any auth secrets first so an OLD
|
|
330
|
+
// transcript can't re-display previously-persisted tokens/passphrases (#454).
|
|
331
|
+
// If redaction changed it, re-save the masked copy so the raw value is gone
|
|
332
|
+
// from storage and never re-persisted later.
|
|
246
333
|
try {
|
|
247
334
|
const saved = await loadScrollback(name);
|
|
248
335
|
if (saved) {
|
|
249
|
-
|
|
336
|
+
const redacted = redactTerminalSecrets(saved);
|
|
337
|
+
term.write(redacted);
|
|
338
|
+
if (redacted !== saved) saveScrollback(name, redacted).catch(() => {});
|
|
250
339
|
}
|
|
251
340
|
} catch { /* ignore */ }
|
|
252
341
|
|
|
@@ -330,15 +419,16 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
330
419
|
|
|
331
420
|
/** Rename a session key (e.g. _new_123 → paper-chair) without killing the PTY.
|
|
332
421
|
* Returns true on success, false on failure. */
|
|
333
|
-
const renameSession = useCallback(async (oldName: string, newName: string): Promise<boolean> => {
|
|
422
|
+
const renameSession = useCallback(async (oldName: string, newName: string, meta?: RenameMeta): Promise<boolean> => {
|
|
334
423
|
const session = sessions.get(oldName);
|
|
335
424
|
if (!session || sessions.has(newName)) return false;
|
|
336
425
|
|
|
337
|
-
// Rename on the server first
|
|
426
|
+
// Rename on the server first. Forward the confirmed story's metadata so the
|
|
427
|
+
// server persists contentType/provider atomically with the rename (#295).
|
|
338
428
|
const res = await authFetchRef.current("/api/terminal/rename", {
|
|
339
429
|
method: "POST",
|
|
340
430
|
headers: { "Content-Type": "application/json" },
|
|
341
|
-
body: JSON.stringify({ oldName, newName }),
|
|
431
|
+
body: JSON.stringify({ oldName, newName, ...(meta ?? {}) }),
|
|
342
432
|
});
|
|
343
433
|
if (!res.ok) return false;
|
|
344
434
|
|
|
@@ -363,6 +453,24 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
363
453
|
return next;
|
|
364
454
|
});
|
|
365
455
|
|
|
456
|
+
// #377: the renamed session's live PTY keeps the cwd it was SPAWNED in, so
|
|
457
|
+
// after the story folder moves (e.g. _new_* → final, or a partial slug →
|
|
458
|
+
// full title) the agent's trust prompt / working directory still points at
|
|
459
|
+
// the old (now-renamed) folder — confusing, and the old folder may no longer
|
|
460
|
+
// exist. If this session is live, move the terminal into the FINAL folder:
|
|
461
|
+
// kill the stale-cwd PTY and reconnect WITH RESUME, so the new spawn runs in
|
|
462
|
+
// stories/<newName> (correct cwd/trust prompt) while the conversation is
|
|
463
|
+
// preserved (claude --resume / codex resume). The server reads the provider
|
|
464
|
+
// from the .story.json it just persisted, so the respawn stays provider-aware.
|
|
465
|
+
// If resume can't recover, the existing code-4000 path reconnects fresh in
|
|
466
|
+
// the same (correct) folder. A never-connected session needs no respawn — its
|
|
467
|
+
// first connect already uses the final name.
|
|
468
|
+
if (session.connected || session.ws) {
|
|
469
|
+
await authFetchRef.current(`/api/terminal/${encodeURIComponent(newName)}`, { method: "DELETE" }).catch(() => {});
|
|
470
|
+
if (session.ws) { session.ws.close(); session.ws = null; }
|
|
471
|
+
connectWsRef.current(newName, session, true);
|
|
472
|
+
}
|
|
473
|
+
|
|
366
474
|
return true;
|
|
367
475
|
}, []);
|
|
368
476
|
|
|
@@ -375,6 +483,20 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
375
483
|
// Auto-spawn + show/hide when story changes
|
|
376
484
|
useEffect(() => {
|
|
377
485
|
if (!storyName) return;
|
|
486
|
+
// Cartoon readiness gate: never spawn/connect a terminal for a cartoon
|
|
487
|
+
// story whose Codex/image_generation is known-not-ready. Show guidance
|
|
488
|
+
// instead (rendered below). Fail-open when readiness is null/undefined.
|
|
489
|
+
if (cartoonLaunchBlocked) {
|
|
490
|
+
showSession(null);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Legacy cartoon with no recorded provider: do NOT auto-spawn. Show the
|
|
494
|
+
// explicit repair CTA so the writer sets the provider to Codex first. After
|
|
495
|
+
// repair, `needsProviderRepair` flips false and normal gating/launch applies.
|
|
496
|
+
if (showProviderRepair) {
|
|
497
|
+
showSession(null);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
378
500
|
if (!sessions.has(storyName)) {
|
|
379
501
|
// Check if a previous session exists — if so, show overlay instead of auto-connecting
|
|
380
502
|
authFetchRef.current(`/api/terminal/session/${encodeURIComponent(storyName)}`)
|
|
@@ -395,7 +517,7 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
395
517
|
} else {
|
|
396
518
|
showSession(storyName);
|
|
397
519
|
}
|
|
398
|
-
}, [storyName, createSession, showSession]);
|
|
520
|
+
}, [storyName, createSession, showSession, cartoonLaunchBlocked, showProviderRepair]);
|
|
399
521
|
|
|
400
522
|
// Periodic scrollback save (every 30s for active session)
|
|
401
523
|
useEffect(() => {
|
|
@@ -496,7 +618,7 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
496
618
|
<div ref={wrapperRef} className="h-full" />
|
|
497
619
|
|
|
498
620
|
{/* Empty state overlay */}
|
|
499
|
-
{isEmpty && (
|
|
621
|
+
{isEmpty && !cartoonLaunchBlocked && !showProviderRepair && (
|
|
500
622
|
<div className="absolute inset-0 flex items-center justify-center text-muted">
|
|
501
623
|
<div className="text-center">
|
|
502
624
|
<p className="text-lg font-serif">Select a story on the left menu</p>
|
|
@@ -505,6 +627,106 @@ export function TerminalPanel({ token, storyName, authFetch, onSelectStory, onDe
|
|
|
505
627
|
</div>
|
|
506
628
|
)}
|
|
507
629
|
|
|
630
|
+
{/* Cartoon launch gated: Codex / image generation not ready */}
|
|
631
|
+
{cartoonLaunchBlocked && (
|
|
632
|
+
<div
|
|
633
|
+
data-testid="cartoon-launch-blocked"
|
|
634
|
+
className="absolute inset-0 flex items-center justify-center"
|
|
635
|
+
style={{ background: "rgba(240, 235, 225, 0.9)" }}
|
|
636
|
+
>
|
|
637
|
+
<div className="space-y-3 p-6 bg-surface border border-border rounded-lg shadow-lg max-w-md">
|
|
638
|
+
<p className="text-sm font-serif text-foreground font-medium">
|
|
639
|
+
Cartoon agent can't launch yet
|
|
640
|
+
</p>
|
|
641
|
+
<p className="text-xs text-muted">
|
|
642
|
+
This is a cartoon story. The writing agent needs Codex with image
|
|
643
|
+
generation enabled before it can start, because the clean-image
|
|
644
|
+
step relies on image generation support.
|
|
645
|
+
</p>
|
|
646
|
+
{readiness && !readiness.codex.installed ? (
|
|
647
|
+
<p className="text-xs text-amber-700">
|
|
648
|
+
Codex was not detected. Install the Codex CLI and sign in
|
|
649
|
+
(e.g. <span className="font-mono">npm i -g @openai/codex</span> then{" "}
|
|
650
|
+
<span className="font-mono">codex login</span>), then reopen this story.
|
|
651
|
+
</p>
|
|
652
|
+
) : isCodexAuthUnclear(readiness) ? (
|
|
653
|
+
<p className="text-xs text-amber-700" data-testid="codex-auth-unknown-launch">
|
|
654
|
+
{CODEX_AUTH_UNCLEAR_MESSAGE} Then reopen this story.
|
|
655
|
+
</p>
|
|
656
|
+
) : (
|
|
657
|
+
<div className="space-y-1">
|
|
658
|
+
<p className="text-xs text-amber-700">
|
|
659
|
+
Codex is installed but image generation isn't enabled. Enable
|
|
660
|
+
it, then reopen this story:
|
|
661
|
+
</p>
|
|
662
|
+
<div className="flex items-center gap-1">
|
|
663
|
+
<code className="flex-1 truncate rounded border border-border bg-surface px-1.5 py-1 text-left text-[10px] font-mono text-foreground">
|
|
664
|
+
{CODEX_ENABLE_CMD}
|
|
665
|
+
</code>
|
|
666
|
+
<button
|
|
667
|
+
type="button"
|
|
668
|
+
data-testid="copy-codex-enable-launch"
|
|
669
|
+
onClick={async () => {
|
|
670
|
+
try {
|
|
671
|
+
await navigator.clipboard.writeText(CODEX_ENABLE_CMD);
|
|
672
|
+
setCopiedEnableCmd(true);
|
|
673
|
+
setTimeout(() => setCopiedEnableCmd(false), 2000);
|
|
674
|
+
} catch { /* clipboard unavailable */ }
|
|
675
|
+
}}
|
|
676
|
+
className="rounded border border-border px-2 py-1 text-[10px] text-muted hover:border-accent hover:text-accent transition-colors"
|
|
677
|
+
>
|
|
678
|
+
{copiedEnableCmd ? "Copied!" : "Copy"}
|
|
679
|
+
</button>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
{/* Legacy cartoon: no provider recorded — explicit, scoped repair CTA.
|
|
688
|
+
Separate from readiness gating; about a MISSING provider on this one
|
|
689
|
+
story. Setting it to Codex never touches other stories or fiction. */}
|
|
690
|
+
{showProviderRepair && !cartoonLaunchBlocked && (
|
|
691
|
+
<div
|
|
692
|
+
data-testid="legacy-cartoon-provider-repair"
|
|
693
|
+
className="absolute inset-0 flex items-center justify-center"
|
|
694
|
+
style={{ background: "rgba(240, 235, 225, 0.9)" }}
|
|
695
|
+
>
|
|
696
|
+
<div className="space-y-3 p-6 bg-surface border border-border rounded-lg shadow-lg max-w-md">
|
|
697
|
+
<p className="text-sm font-serif text-foreground font-medium">
|
|
698
|
+
Set this cartoon story's provider
|
|
699
|
+
</p>
|
|
700
|
+
<p className="text-xs text-muted">
|
|
701
|
+
This cartoon story was created before provider tracking, so it has
|
|
702
|
+
no provider recorded and would launch with Claude — which can't
|
|
703
|
+
generate the clean images cartoons need. Set this story's
|
|
704
|
+
provider to Codex to continue.
|
|
705
|
+
</p>
|
|
706
|
+
<p className="text-[11px] text-muted">
|
|
707
|
+
Only this story is changed. Other stories and fiction are not affected.
|
|
708
|
+
</p>
|
|
709
|
+
<button
|
|
710
|
+
type="button"
|
|
711
|
+
data-testid="repair-provider-codex"
|
|
712
|
+
disabled={repairing}
|
|
713
|
+
onClick={async () => {
|
|
714
|
+
if (repairing) return;
|
|
715
|
+
setRepairing(true);
|
|
716
|
+
try {
|
|
717
|
+
await onRepairProvider?.();
|
|
718
|
+
} finally {
|
|
719
|
+
setRepairing(false);
|
|
720
|
+
}
|
|
721
|
+
}}
|
|
722
|
+
className="px-4 py-1.5 bg-accent text-white text-sm rounded hover:bg-accent-dim disabled:opacity-50 disabled:cursor-not-allowed"
|
|
723
|
+
>
|
|
724
|
+
{repairing ? "Setting…" : "Set this story's provider to Codex"}
|
|
725
|
+
</button>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
|
|
508
730
|
{/* Discard confirmation overlay */}
|
|
509
731
|
{confirmingDiscard && (
|
|
510
732
|
<div className="absolute inset-0 flex items-center justify-center z-10" style={{ background: "rgba(240, 235, 225, 0.9)" }}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
const API_BASE = "http://localhost:7777";
|
|
4
4
|
|
|
@@ -7,29 +7,45 @@ interface WalletInfo {
|
|
|
7
7
|
walletId?: string;
|
|
8
8
|
name?: string;
|
|
9
9
|
address?: string;
|
|
10
|
+
activeWallet?: WalletChoice;
|
|
11
|
+
wallets?: WalletChoice[];
|
|
12
|
+
selectionRequired?: boolean;
|
|
10
13
|
ethBalance?: string;
|
|
11
14
|
usdcBalance?: string;
|
|
12
15
|
plotBalance?: string;
|
|
13
16
|
error?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
interface WalletChoice {
|
|
20
|
+
walletId?: string;
|
|
21
|
+
name: string;
|
|
22
|
+
address?: string;
|
|
23
|
+
normalizedAddress?: string;
|
|
24
|
+
source: "ows";
|
|
25
|
+
label: string;
|
|
26
|
+
recognized: boolean;
|
|
27
|
+
active: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export function WalletCard({ token }: { token: string }) {
|
|
17
31
|
const [wallet, setWallet] = useState<WalletInfo | null>(null);
|
|
18
32
|
const [creating, setCreating] = useState(false);
|
|
33
|
+
const [switching, setSwitching] = useState<string | null>(null);
|
|
19
34
|
const [copied, setCopied] = useState(false);
|
|
20
35
|
const [error, setError] = useState<string | null>(null);
|
|
21
36
|
|
|
22
|
-
const authFetch = (url: string, opts?: RequestInit) =>
|
|
23
|
-
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } })
|
|
37
|
+
const authFetch = useCallback((url: string, opts?: RequestInit) =>
|
|
38
|
+
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }),
|
|
39
|
+
[token]);
|
|
24
40
|
|
|
25
|
-
const loadWallet = () => {
|
|
41
|
+
const loadWallet = useCallback(() => {
|
|
26
42
|
authFetch(`${API_BASE}/api/wallet`)
|
|
27
43
|
.then((r) => r.json())
|
|
28
44
|
.then((data) => setWallet(data))
|
|
29
45
|
.catch(() => setWallet({ exists: false, error: "Failed to load wallet" }));
|
|
30
|
-
};
|
|
46
|
+
}, [authFetch]);
|
|
31
47
|
|
|
32
|
-
useEffect(() => { loadWallet(); }, []);
|
|
48
|
+
useEffect(() => { loadWallet(); }, [loadWallet]);
|
|
33
49
|
|
|
34
50
|
const handleCreate = async () => {
|
|
35
51
|
setCreating(true);
|
|
@@ -45,6 +61,27 @@ export function WalletCard({ token }: { token: string }) {
|
|
|
45
61
|
setCreating(false);
|
|
46
62
|
};
|
|
47
63
|
|
|
64
|
+
const handleSwitch = async (choice: WalletChoice) => {
|
|
65
|
+
setSwitching(choice.walletId || choice.name);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const res = await authFetch(`${API_BASE}/api/wallet/active`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
walletId: choice.walletId,
|
|
72
|
+
name: choice.name,
|
|
73
|
+
address: choice.normalizedAddress || choice.address,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
if (!res.ok) throw new Error(data.error || "Wallet switch failed");
|
|
78
|
+
loadWallet();
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
setError(err instanceof Error ? err.message : "Failed to switch wallet");
|
|
81
|
+
}
|
|
82
|
+
setSwitching(null);
|
|
83
|
+
};
|
|
84
|
+
|
|
48
85
|
const copyAddress = () => {
|
|
49
86
|
if (wallet?.address) {
|
|
50
87
|
navigator.clipboard.writeText(wallet.address);
|
|
@@ -63,7 +100,7 @@ export function WalletCard({ token }: { token: string }) {
|
|
|
63
100
|
|
|
64
101
|
{wallet && !wallet.exists && (
|
|
65
102
|
<div className="space-y-3">
|
|
66
|
-
<p className="text-muted text-xs">No wallet created yet. Create one to enable autonomous transactions
|
|
103
|
+
<p className="text-muted text-xs">{wallet.error || "No wallet created yet. Create one to enable autonomous transactions."}</p>
|
|
67
104
|
{error && <p className="text-error text-xs">{error}</p>}
|
|
68
105
|
<button
|
|
69
106
|
onClick={handleCreate}
|
|
@@ -75,15 +112,44 @@ export function WalletCard({ token }: { token: string }) {
|
|
|
75
112
|
</div>
|
|
76
113
|
)}
|
|
77
114
|
|
|
115
|
+
{wallet?.selectionRequired && wallet.wallets && wallet.wallets.length > 0 && (
|
|
116
|
+
<div className="mb-4 space-y-3 rounded border border-amber-600/30 bg-amber-950/10 p-3">
|
|
117
|
+
<p className="text-xs text-amber-700">Multiple OWS wallets found. Select the wallet OWS should use for publishing and signing.</p>
|
|
118
|
+
{wallet.wallets.map((choice) => (
|
|
119
|
+
<div key={choice.walletId || choice.name} className="border-border flex items-center justify-between gap-3 rounded border p-2">
|
|
120
|
+
<div className="min-w-0">
|
|
121
|
+
<p className="text-foreground truncate text-xs font-medium">{choice.name}</p>
|
|
122
|
+
<p className="text-muted truncate text-[10px] font-mono">{choice.address || "No EVM address"}</p>
|
|
123
|
+
</div>
|
|
124
|
+
<button
|
|
125
|
+
onClick={() => handleSwitch(choice)}
|
|
126
|
+
disabled={!choice.address || switching === (choice.walletId || choice.name)}
|
|
127
|
+
className="border-accent text-accent hover:bg-accent/10 disabled:opacity-40 rounded border px-2 py-1 text-[10px] font-medium transition-colors"
|
|
128
|
+
>
|
|
129
|
+
{switching === (choice.walletId || choice.name) ? "switching..." : "use"}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
{error && <p className="text-error text-xs">{error}</p>}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
78
137
|
{wallet && wallet.exists && wallet.address && (
|
|
79
138
|
<div className="space-y-3">
|
|
80
139
|
<div className="flex items-center justify-between">
|
|
81
|
-
<span className="text-muted text-[10px] uppercase tracking-wider">
|
|
140
|
+
<span className="text-muted text-[10px] uppercase tracking-wider">Active Wallet (Base)</span>
|
|
82
141
|
<span className={`rounded border px-1.5 py-0.5 text-[9px] ${wallet.ethBalance && parseFloat(wallet.ethBalance) > 0 ? "border-accent/30 text-accent" : "border-accent-dim/30 text-accent-dim"}`}>
|
|
83
142
|
{wallet.ethBalance && parseFloat(wallet.ethBalance) > 0 ? "active" : "no balance"}
|
|
84
143
|
</span>
|
|
85
144
|
</div>
|
|
86
145
|
|
|
146
|
+
{wallet.name && (
|
|
147
|
+
<div className="flex justify-between text-xs">
|
|
148
|
+
<span className="text-muted">Name</span>
|
|
149
|
+
<span className="text-foreground truncate pl-3 font-mono text-[10px]">{wallet.name}</span>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
87
153
|
<div className="flex items-center gap-2">
|
|
88
154
|
<code className="text-foreground bg-surface rounded px-2 py-1 text-xs font-mono">{truncate(wallet.address)}</code>
|
|
89
155
|
<button onClick={copyAddress} className="text-muted hover:text-accent text-xs transition-colors">
|
|
@@ -110,6 +176,32 @@ export function WalletCard({ token }: { token: string }) {
|
|
|
110
176
|
</div>
|
|
111
177
|
</div>
|
|
112
178
|
|
|
179
|
+
{wallet.wallets && wallet.wallets.length > 1 && (
|
|
180
|
+
<div className="border-border space-y-2 border-t pt-3">
|
|
181
|
+
<p className="text-muted text-[10px] font-medium uppercase tracking-wider">Switch Wallet</p>
|
|
182
|
+
{wallet.wallets.map((choice) => (
|
|
183
|
+
<div key={choice.walletId || choice.name} className="flex items-center justify-between gap-3 text-xs">
|
|
184
|
+
<div className="min-w-0">
|
|
185
|
+
<p className={choice.active ? "text-accent truncate font-medium" : "text-foreground truncate"}>
|
|
186
|
+
{choice.name}{choice.active ? " (active)" : ""}
|
|
187
|
+
</p>
|
|
188
|
+
<p className="text-muted truncate text-[10px] font-mono">{choice.address || "No EVM address"}</p>
|
|
189
|
+
</div>
|
|
190
|
+
{!choice.active && (
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => handleSwitch(choice)}
|
|
193
|
+
disabled={!choice.address || switching === (choice.walletId || choice.name)}
|
|
194
|
+
className="border-border text-muted hover:border-accent hover:text-accent disabled:opacity-40 rounded border px-2 py-1 text-[10px] transition-colors"
|
|
195
|
+
>
|
|
196
|
+
{switching === (choice.walletId || choice.name) ? "..." : "use"}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
{error && <p className="text-error text-xs">{error}</p>}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
113
205
|
{/* Fund wallet */}
|
|
114
206
|
<div className="border-border border-t pt-3">
|
|
115
207
|
<p className="text-muted mb-2 text-[10px] font-medium uppercase tracking-wider">Fund Wallet</p>
|
|
@@ -118,6 +210,16 @@ export function WalletCard({ token }: { token: string }) {
|
|
|
118
210
|
</div>
|
|
119
211
|
</div>
|
|
120
212
|
)}
|
|
213
|
+
|
|
214
|
+
{wallet?.exists && (
|
|
215
|
+
<button
|
|
216
|
+
onClick={handleCreate}
|
|
217
|
+
disabled={creating}
|
|
218
|
+
className="border-border text-muted hover:border-accent hover:text-accent disabled:opacity-40 mt-4 rounded border px-3 py-1.5 text-[10px] font-medium transition-colors"
|
|
219
|
+
>
|
|
220
|
+
{creating ? "creating..." : "create another wallet"}
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
121
223
|
</div>
|
|
122
224
|
);
|
|
123
225
|
}
|