slidemind 0.3.20 → 0.4.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
// Shared browser-side CHROME for the CLI's two blocking gates — the plan
|
|
2
|
+
// sign-off gate (plan-overlay.js) and the spatial preview gate
|
|
3
|
+
// (review-overlay.js). gate-session.js owns the ledger/round machinery; this
|
|
4
|
+
// module owns everything the two pages LOOK like in common: the brand assets
|
|
5
|
+
// (orb + wordmark), the icon set, the top bar, the buttons, the workspace
|
|
6
|
+
// strips under the bar (approved / not asked yet / agent revising), the
|
|
7
|
+
// hand-off dialog, the "round arrived" toast, the done veil and the slide-rail
|
|
8
|
+
// styling. Both overlays used to carry byte-identical copies of all of it; a
|
|
9
|
+
// visual fix that landed in one and not the other is how the two drifted.
|
|
10
|
+
//
|
|
11
|
+
// Plain dependency-free ESM (a test can `import` it); the gates inline it
|
|
12
|
+
// import-free as window.__SLIDEMIND_SHELL__ — see bundleGateSession() in
|
|
13
|
+
// gate-server.mjs, which ships session + shell in one script block.
|
|
14
|
+
//
|
|
15
|
+
// STATE OWNERSHIP: the shell owns the decision on the wire (one attempt at a
|
|
16
|
+
// time, retried on a busy deck or a stopped workspace) and the workspace
|
|
17
|
+
// HAND-OVER after it — the message and the hand-off dialog that says to paste
|
|
18
|
+
// it, the revising strip — because every strip and dialog reads it.
|
|
19
|
+
// The overlay owns its ledger and its mode flags and tells the shell about them
|
|
20
|
+
// through the callbacks it passes to createGateShell.
|
|
21
|
+
//
|
|
22
|
+
// HONESTY RULES (the workspace page; the user is not technical and chats with
|
|
23
|
+
// the agent at the same time): nothing reaches an agent whose turn has ended, so
|
|
24
|
+
// after every answer the page does the same thing — it shows the message that
|
|
25
|
+
// brings the agent back and says "paste this into your agent chat" (doc 21).
|
|
26
|
+
// The clipboard is the user's: only their click on the copy icon writes it,
|
|
27
|
+
// and the page never claims a copy that did not happen. It never says anything about
|
|
28
|
+
// whether some agent process happens to be listening, and "your agent picked it
|
|
29
|
+
// up" appears only once the receipt (`pickedUpAt`) proves it. What the user
|
|
30
|
+
// wrote is never dropped by a failure.
|
|
31
|
+
|
|
32
|
+
// The add-in's embossed "raised slide" mark (src/ui/public/logo.svg), inlined.
|
|
33
|
+
export const LOGO_SVG =
|
|
34
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
|
|
35
|
+
"<defs>" +
|
|
36
|
+
'<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
|
|
37
|
+
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
|
|
38
|
+
'<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
|
|
39
|
+
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
40
|
+
'<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
|
|
41
|
+
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
|
|
42
|
+
'<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
|
|
43
|
+
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
44
|
+
'<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
|
|
45
|
+
'<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
|
|
46
|
+
'<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
|
|
47
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
48
|
+
'<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
|
|
49
|
+
'<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
|
|
50
|
+
'<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
|
|
51
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
52
|
+
'<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
|
|
53
|
+
'<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
|
|
54
|
+
'<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
|
|
55
|
+
'<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
|
|
56
|
+
"</defs>" +
|
|
57
|
+
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
|
|
58
|
+
'<path d="M 160,200 A 40,40 0 0 1 200,160 L 1080,160 A 40,40 0 0 1 1120,200" fill="none" stroke="url(#rimTop)" stroke-width="3" stroke-linecap="round"/>' +
|
|
59
|
+
'<path d="M 160,200 L 160,660 A 40,40 0 0 0 200,700" fill="none" stroke="url(#rimLeft)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
60
|
+
'<path d="M 1120,660 A 40,40 0 0 1 1080,700 L 200,700 A 40,40 0 0 1 160,660" fill="none" stroke="url(#rimBottom)" stroke-width="3" stroke-linecap="round"/>' +
|
|
61
|
+
'<path d="M 1120,660 L 1120,200 A 40,40 0 0 0 1080,160" fill="none" stroke="url(#rimRight)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
62
|
+
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.4"/>' +
|
|
63
|
+
"</svg>";
|
|
64
|
+
|
|
65
|
+
// Lucide icons (MIT) — the family the add-in's dialogs use.
|
|
66
|
+
export const ICONS = {
|
|
67
|
+
copy:
|
|
68
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
|
69
|
+
check:
|
|
70
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
71
|
+
message:
|
|
72
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M7 9h10"/><path d="M7 13h6"/></svg>',
|
|
73
|
+
trash:
|
|
74
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>',
|
|
75
|
+
close:
|
|
76
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></svg>',
|
|
77
|
+
pin:
|
|
78
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.4-8 12-8 12s-8-7.6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>',
|
|
79
|
+
arrow:
|
|
80
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
|
|
81
|
+
send:
|
|
82
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>',
|
|
83
|
+
sparkles:
|
|
84
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
|
|
85
|
+
plus:
|
|
86
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
|
|
87
|
+
x:
|
|
88
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
89
|
+
ruler:
|
|
90
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
|
|
91
|
+
grip:
|
|
92
|
+
'<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg>',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// The chrome CSS both gates inject: tokens, bar, buttons, strips, dialogs,
|
|
96
|
+
// veil, and the slide rail (`.sm-rail`, the plan gate's own rail; the preview
|
|
97
|
+
// page brings its `.deck__rail`, which the same rules style through the
|
|
98
|
+
// `--deck-chrome-*` tokens).
|
|
99
|
+
export const SHELL_CSS =
|
|
100
|
+
"@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
|
|
101
|
+
"@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
|
|
102
|
+
"@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
|
|
103
|
+
"@keyframes sm-pop-in{from{opacity:0;transform:translateY(5px) scale(.98)}to{opacity:1;transform:none}}" +
|
|
104
|
+
"@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
|
|
105
|
+
"@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
|
|
106
|
+
"@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
|
|
107
|
+
"@keyframes sm-spin{to{transform:rotate(360deg)}}" +
|
|
108
|
+
":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);--sm-surface-2:#1c1c20;" +
|
|
109
|
+
"--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
|
|
110
|
+
"--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-muted:rgba(255,255,255,.05);--sm-accent:#a9c2ff;--sm-success:#2ecc71;--sm-danger:#f87171;--sm-warn:#f6c454;" +
|
|
111
|
+
"--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);--sm-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--sm-bar-h:62px;--sm-rail-w:220px;}" +
|
|
112
|
+
".sm-o,.sm-o *{box-sizing:border-box;}" +
|
|
113
|
+
/* hosted in the workspace page: the page shows this gate's messages (publishChrome) — the bar stays, its status line and every strip go */
|
|
114
|
+
".sm-hosted .sm-waiter,.sm-hosted .sm-strips,.sm-hosted .sm-arrived,.sm-hosted .sm-handoff{display:none!important;}" +
|
|
115
|
+
/* ...and the page's own top bar already carries the mark: the bar starts at its title */
|
|
116
|
+
".sm-hosted .sm-bar .sm-brand,.sm-hosted .sm-bar>.sm-sep{display:none;}" +
|
|
117
|
+
/* header */
|
|
118
|
+
".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
|
|
119
|
+
"gap:16px;height:var(--sm-bar-h,62px);padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
|
|
120
|
+
"background:var(--sm-bg);border-bottom:1px solid var(--sm-border);" +
|
|
121
|
+
"user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
|
|
122
|
+
".sm-brand{display:flex;align-items:center;gap:11px;}" +
|
|
123
|
+
".sm-logo{position:relative;width:48px;height:27px;border-radius:5px;overflow:hidden;flex-shrink:0;background:#0a0c11;box-shadow:0 1px 3px rgba(0,0,0,.5);}" +
|
|
124
|
+
".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
|
|
125
|
+
".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
|
|
126
|
+
".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
|
|
127
|
+
"font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
|
|
128
|
+
".sm-word b{font-weight:300;display:block;will-change:transform;}" +
|
|
129
|
+
".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
|
|
130
|
+
".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
|
|
131
|
+
".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
|
|
132
|
+
".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;}" +
|
|
133
|
+
".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;white-space:nowrap;}" +
|
|
134
|
+
/* the bar's notes fold into an (i) when the middle of the bar has no room */
|
|
135
|
+
".sm-info{display:none;align-items:center;justify-content:center;width:17px;height:17px;flex-shrink:0;padding:0;border-radius:50%;border:1px solid var(--sm-border);background:transparent;color:var(--sm-dim);font:600 10px/1 var(--sm-font);cursor:pointer;}" +
|
|
136
|
+
".sm-info:hover{color:var(--sm-fg);border-color:#4a4a52;}.sm-bar.is-tight .sm-info.has{display:inline-flex;}" +
|
|
137
|
+
".sm-info-pop{position:fixed;z-index:2147483630;max-width:min(380px,calc(100vw - 24px));padding:12px 14px;border-radius:12px;background:var(--sm-surface);border:1px solid var(--sm-border);box-shadow:0 18px 50px rgba(0,0,0,.55);font:12px/1.5 var(--sm-font);color:#d4d4d8;animation:sm-pop-in .14s ease;}" +
|
|
138
|
+
".sm-info-pop p{margin:0;}.sm-info-pop p+p{margin-top:8px;}.sm-info-pop b{color:#f4f4f5;font-weight:600;}" +
|
|
139
|
+
".sm-meta .h{font-size:11.5px;color:var(--sm-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}" +
|
|
140
|
+
".sm-grow{flex:1;min-width:0;}" +
|
|
141
|
+
".sm-edited{padding:1px 7px;border-radius:999px;font-size:9.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;border:1px solid rgba(169,194,255,.45);color:var(--sm-accent);}" +
|
|
142
|
+
".sm-note{max-width:min(560px,38vw);min-width:0;font-size:10.5px;line-height:1.35;color:#c7c7cf;border-left:2px solid var(--sm-accent);padding-left:10px;" +
|
|
143
|
+
"overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;}" +
|
|
144
|
+
".sm-bar.is-tight .sm-grow .sm-note{display:none;}" +
|
|
145
|
+
".sm-note b{font-weight:600;color:#f4f4f5;}" +
|
|
146
|
+
".sm-count{font-size:12px;color:var(--sm-dim);font-variant-numeric:tabular-nums;white-space:nowrap;}" +
|
|
147
|
+
".sm-waiter{display:flex;align-items:center;gap:8px;flex-shrink:0;max-width:250px;font-size:11px;line-height:1.3;color:var(--sm-dim);}" +
|
|
148
|
+
".sm-waiter .dot{width:7px;height:7px;flex-shrink:0;border-radius:50%;background:#71717a;}" +
|
|
149
|
+
".sm-waiter.on{color:#c6d4f5;}.sm-waiter.on .dot{background:var(--sm-success);box-shadow:0 0 0 3px rgba(46,204,113,.2);}" +
|
|
150
|
+
".sm-waiter.warn{color:#fde68a;}.sm-waiter.warn .dot{background:var(--sm-warn);animation:sm-pulse 1.4s ease-in-out infinite;}" +
|
|
151
|
+
".sm-waiter.err{color:#fecaca;}.sm-waiter.err .dot{background:var(--sm-danger);}" +
|
|
152
|
+
".sm-waiter.act{cursor:pointer;}.sm-waiter.act:hover{color:var(--sm-fg);}" +
|
|
153
|
+
/* the bar at narrow widths: the brand and the secondary texts give way first */
|
|
154
|
+
"@media (max-width:1000px){.sm-meta .h{display:none;}.sm-count{display:none;}}" +
|
|
155
|
+
"@media (max-width:760px){.sm-bar{gap:10px;padding:0 12px;}.sm-word,.sm-sep{display:none;}.sm-waiter{max-width:170px;}}" +
|
|
156
|
+
/* the strips under the bar (workspace states + notices), stacked in one
|
|
157
|
+
fixed host so two of them can never paint over each other */
|
|
158
|
+
".sm-strips{position:fixed;left:0;right:0;top:var(--sm-bar-h,62px);z-index:2147483610;display:flex;flex-direction:column;}" +
|
|
159
|
+
".sm-strips:empty{display:none;}" +
|
|
160
|
+
".sm-notice{order:1;display:flex;align-items:center;gap:12px;padding:10px 20px;font-family:var(--sm-font);color:var(--sm-fg);" +
|
|
161
|
+
"background:linear-gradient(90deg,rgba(169,194,255,.16),rgba(169,194,255,.06));border-bottom:1px solid rgba(169,194,255,.4);animation:sm-rise .3s cubic-bezier(.2,.7,.2,1);}" +
|
|
162
|
+
".sm-notice>svg{width:18px;height:18px;flex-shrink:0;color:var(--sm-accent);}" +
|
|
163
|
+
".sm-notice .txt{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1;font-size:12px;line-height:1.45;color:#d7d7de;}.sm-notice .txt b{font-size:13px;color:#f4f4f5;}" +
|
|
164
|
+
".sm-notice .acts{display:flex;align-items:center;gap:8px;flex-shrink:0;}" +
|
|
165
|
+
".sm-notice--warn{background:linear-gradient(90deg,rgba(251,191,36,.16),rgba(251,191,36,.05));border-bottom-color:rgba(251,191,36,.45);}.sm-notice--warn>svg{color:#fbbf24;}.sm-notice--warn .sm-spin{border-color:#fbbf24;border-top-color:transparent;}" +
|
|
166
|
+
".sm-notice--error{background:linear-gradient(90deg,rgba(248,113,113,.18),rgba(248,113,113,.06));border-bottom-color:rgba(248,113,113,.5);}.sm-notice--error>svg{color:var(--sm-danger);}" +
|
|
167
|
+
".sm-notice--ok{background:linear-gradient(90deg,rgba(46,204,113,.16),rgba(46,204,113,.06));border-bottom-color:rgba(46,204,113,.4);}.sm-notice--ok>svg{color:var(--sm-success);}" +
|
|
168
|
+
".sm-notice-x{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--sm-dim);cursor:pointer;}" +
|
|
169
|
+
".sm-notice-x svg{width:14px;height:14px;}.sm-notice-x:hover{background:rgba(255,255,255,.08);color:var(--sm-fg);}" +
|
|
170
|
+
".sm-approved-banner{order:2;display:flex;align-items:center;gap:12px;padding:10px 20px;" +
|
|
171
|
+
"background:linear-gradient(90deg,rgba(46,204,113,.16),rgba(46,204,113,.06));border-bottom:1px solid rgba(46,204,113,.4);font-family:var(--sm-font);color:var(--sm-fg);}" +
|
|
172
|
+
".sm-approved-banner svg{width:18px;height:18px;color:var(--sm-success);flex-shrink:0;}" +
|
|
173
|
+
".sm-approved-banner .txt{display:flex;flex-direction:column;gap:1px;font-size:12px;color:#d7d7de;}.sm-approved-banner .txt b{font-size:13px;color:#f4f4f5;}" +
|
|
174
|
+
".sm-notasked{background:linear-gradient(90deg,rgba(251,191,36,.16),rgba(251,191,36,.05));border-bottom-color:rgba(251,191,36,.45);}.sm-notasked svg{color:#fbbf24;}" +
|
|
175
|
+
".sm-revising{order:3;display:flex;align-items:center;gap:14px;padding:13px 20px;" +
|
|
176
|
+
"background:linear-gradient(90deg,rgba(169,194,255,.18),rgba(169,194,255,.08));border-bottom:1px solid rgba(169,194,255,.4);" +
|
|
177
|
+
"-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-family:var(--sm-font);color:var(--sm-fg);animation:sm-rise .3s cubic-bezier(.2,.7,.2,1);}" +
|
|
178
|
+
".sm-revising .txt{display:flex;flex-direction:column;gap:2px;font-size:12.5px;color:#d7d7de;}.sm-revising .txt b{font-size:14px;color:#f4f4f5;}" +
|
|
179
|
+
".sm-revising>.ic{display:flex;flex-shrink:0;}.sm-revising>.ic svg{width:20px;height:20px;color:var(--sm-accent);}" +
|
|
180
|
+
".sm-revising .txt{flex:1;min-width:0;}" +
|
|
181
|
+
".sm-revising--warn{background:linear-gradient(90deg,rgba(251,191,36,.18),rgba(251,191,36,.06));border-bottom-color:rgba(251,191,36,.45);}.sm-revising--warn>.ic svg{color:#fbbf24;}" +
|
|
182
|
+
".sm-revising--ok{background:linear-gradient(90deg,rgba(46,204,113,.16),rgba(46,204,113,.06));border-bottom-color:rgba(46,204,113,.4);}.sm-revising--ok>.ic svg{color:var(--sm-success);}" +
|
|
183
|
+
".sm-spin{width:11px;height:11px;flex-shrink:0;border-radius:50%;border:2px solid var(--sm-accent);border-top-color:transparent;animation:sm-spin .9s linear infinite;}" +
|
|
184
|
+
".sm-spin-lg{width:20px;height:20px;border-width:2.5px;}" +
|
|
185
|
+
".sm-copy-msg{display:inline-flex;align-items:center;align-self:flex-start;gap:7px;margin-top:8px;border:1px solid var(--sm-border);border-radius:9px;padding:7px 12px;background:transparent;color:var(--sm-fg);font:600 12px var(--sm-font);cursor:pointer;}" +
|
|
186
|
+
".sm-copy-msg svg{width:14px;height:14px;}.sm-copy-msg:hover{background:rgba(255,255,255,.06);}" +
|
|
187
|
+
".sm-copy-veil{margin-top:0;animation:sm-rise .6s .24s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
188
|
+
".sm-say{display:inline-block;padding:.02em .5em;border-radius:7px;background:rgba(169,194,255,.14);border:1px solid rgba(169,194,255,.42);color:#f4f4f5;font-weight:600;}" +
|
|
189
|
+
/* the "round arrived" toast */
|
|
190
|
+
".sm-arrived{position:fixed;left:50%;top:calc(var(--deck-chrome-h,62px) + 14px);transform:translateX(-50%);z-index:2147483620;display:flex;align-items:center;gap:12px;padding:12px 18px;border-radius:12px;" +
|
|
191
|
+
"background:rgba(22,22,24,.96);border:1px solid rgba(46,204,113,.5);box-shadow:0 18px 54px rgba(0,0,0,.55);font-family:var(--sm-font);color:var(--sm-fg);animation:sm-rise .35s cubic-bezier(.2,.7,.2,1);transition:opacity .4s;}" +
|
|
192
|
+
".sm-arrived.out{opacity:0;}.sm-arrived svg{width:20px;height:20px;color:var(--sm-success);flex-shrink:0;}" +
|
|
193
|
+
".sm-arrived .txt{display:flex;flex-direction:column;gap:2px;font-size:12.5px;color:#d7d7de;}.sm-arrived .txt b{font-size:14px;color:#f4f4f5;}" +
|
|
194
|
+
/* hand-off dialog */
|
|
195
|
+
".sm-handoff{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;background:rgba(5,5,7,.72);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);animation:sm-veil-in .2s ease;font-family:var(--sm-font);}" +
|
|
196
|
+
".sm-handoff-box{width:min(560px,calc(100vw - 32px));background:var(--sm-surface);color:var(--sm-fg);border:1px solid var(--sm-border);border-radius:16px;padding:22px 24px;box-shadow:0 24px 64px rgba(0,0,0,.6);animation:sm-rise .3s cubic-bezier(.2,.7,.2,1);}" +
|
|
197
|
+
".sm-handoff-head{display:flex;align-items:center;gap:9px;font-weight:600;font-size:16px;margin-bottom:10px;}.sm-handoff-head svg{width:20px;height:20px;color:var(--sm-success);}" +
|
|
198
|
+
".sm-handoff-p{margin:0 0 12px;font-size:13.5px;line-height:1.5;color:#d4d4d8;}.sm-handoff-p b{color:#f4f4f5;}" +
|
|
199
|
+
".sm-handoff-p.big{font-size:15px;color:#ececf0;}" +
|
|
200
|
+
".sm-handoff-sub{margin:0 0 18px;font-size:12.5px;line-height:1.5;color:var(--sm-dim);}" +
|
|
201
|
+
".sm-msgbox{position:relative;margin:0 0 16px;}" +
|
|
202
|
+
".sm-copy-icon{position:absolute;top:7px;right:7px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:7px;background:transparent;color:var(--sm-dim);cursor:pointer;}" +
|
|
203
|
+
".sm-copy-icon:hover{background:rgba(255,255,255,.08);color:var(--sm-fg);}.sm-copy-icon svg{width:15px;height:15px;}.sm-copy-icon.is-copied{color:var(--sm-success);}" +
|
|
204
|
+
".sm-handoff-msg{display:block;box-sizing:border-box;width:100%;margin:0;padding:10px 44px 10px 12px;resize:none;border:1px solid var(--sm-border);border-radius:9px;background:rgba(255,255,255,.04);color:#ececf0;font:12.5px/1.5 var(--sm-font);}" +
|
|
205
|
+
".sm-handoff-row{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;}" +
|
|
206
|
+
".sm-handoff-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;padding:8px 14px;font:600 12.5px var(--sm-font);cursor:pointer;}" +
|
|
207
|
+
".sm-handoff-btn svg{width:15px;height:15px;}" +
|
|
208
|
+
".sm-handoff-btn.ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}.sm-handoff-btn.ghost:hover{background:rgba(255,255,255,.06);}" +
|
|
209
|
+
".sm-handoff-btn.primary{background:#f4f4f5;color:#0f0f0f;}.sm-handoff-btn.primary:hover{background:#fff;}" +
|
|
210
|
+
/* buttons */
|
|
211
|
+
".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
|
|
212
|
+
"padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
|
|
213
|
+
"transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
|
|
214
|
+
".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
|
|
215
|
+
".sm-btn-primary{background:#f4f4f5;color:#0f0f0f;}" +
|
|
216
|
+
".sm-btn-primary:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
|
|
217
|
+
".sm-btn-primary svg{color:var(--sm-success);}" +
|
|
218
|
+
".sm-btn-ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}" +
|
|
219
|
+
".sm-btn-ghost:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
|
|
220
|
+
".sm-btn-danger{background:transparent;border-color:rgba(248,113,113,.4);color:var(--sm-danger);}" +
|
|
221
|
+
".sm-btn-danger:hover{background:rgba(248,113,113,.12);}" +
|
|
222
|
+
".sm-btn.is-off{opacity:.4;cursor:not-allowed;pointer-events:none;}" +
|
|
223
|
+
/* done veil */
|
|
224
|
+
".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;" +
|
|
225
|
+
"justify-content:center;gap:30px;font-family:var(--sm-font);color:var(--sm-fg);" +
|
|
226
|
+
"background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
|
|
227
|
+
"-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
|
|
228
|
+
".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
229
|
+
".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;" +
|
|
230
|
+
"font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
231
|
+
".sm-veil .word b{font-weight:300;}" +
|
|
232
|
+
".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
|
|
233
|
+
".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
|
|
234
|
+
".sm-veil .status{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;animation:sm-rise .6s .12s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
235
|
+
".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
|
|
236
|
+
".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
237
|
+
".sm-veil .sub.sm-pulse{animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both,sm-pulse 2s .9s ease-in-out infinite;}" +
|
|
238
|
+
/* the slide rail: the plan gate's own list; the preview page's .deck__rail
|
|
239
|
+
shares the widths and the card look through the --deck-chrome-* tokens */
|
|
240
|
+
".sm-rail{display:flex;flex-direction:column;min-height:0;background:var(--sm-bg);border-right:1px solid var(--sm-border);font-family:var(--sm-font);color:var(--sm-fg);}" +
|
|
241
|
+
".sm-rail-head{display:flex;align-items:center;gap:8px;padding:12px 14px 8px;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--sm-dim);}" +
|
|
242
|
+
".sm-rail-head .n{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.7;}" +
|
|
243
|
+
".sm-rail-list{flex:1;min-height:0;overflow-y:auto;padding:4px 14px 18px;margin:0;list-style:none;scrollbar-width:none;}" +
|
|
244
|
+
".sm-rail-list::-webkit-scrollbar{display:none;}" +
|
|
245
|
+
".sm-thumb{position:relative;display:block;width:100%;margin:0 0 10px;padding:8px;border:1px solid var(--sm-border);border-radius:8px;" +
|
|
246
|
+
"background:rgba(255,255,255,.025);color:inherit;cursor:pointer;text-align:left;transition:border-color .12s,background .12s,box-shadow .12s;}" +
|
|
247
|
+
".sm-thumb:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
|
|
248
|
+
".sm-thumb.is-active{border-color:rgba(237,237,237,.7);background:rgba(255,255,255,.085);box-shadow:0 0 0 2px rgba(237,237,237,.08);}" +
|
|
249
|
+
".sm-thumb-preview{position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:3px;background:#fff;box-shadow:0 4px 14px rgba(0,0,0,.28);pointer-events:none;}" +
|
|
250
|
+
".sm-thumb-meta{display:flex;align-items:baseline;gap:7px;margin-top:7px;min-width:0;font-size:11px;line-height:1.3;}" +
|
|
251
|
+
".sm-thumb-number{flex:0 0 auto;color:var(--sm-dim);font-variant-numeric:tabular-nums;}" +
|
|
252
|
+
".sm-thumb-name{min-width:0;flex:1;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}" +
|
|
253
|
+
".sm-thumb-sub{margin-top:3px;font-size:10px;color:var(--sm-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}";
|
|
254
|
+
|
|
255
|
+
// Give a copy of the logo unique gradient ids so two instances (header + done
|
|
256
|
+
// veil) can coexist without id collisions.
|
|
257
|
+
export function logoWithIds(suffix) {
|
|
258
|
+
return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(/url\(#([^)]+)\)/g, "url(#$1" + suffix + ")");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function el(tag, cls, html) {
|
|
262
|
+
const n = document.createElement(tag);
|
|
263
|
+
if (cls) n.className = cls;
|
|
264
|
+
if (html != null) n.innerHTML = html;
|
|
265
|
+
return n;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function wordmarkHtml() {
|
|
269
|
+
return '<b class="top">slide</b><b class="bot">mind</b>';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// ---------- time words (pure — every strip, toast and dialog prints these) ----------
|
|
274
|
+
|
|
275
|
+
// hh:mm on the 24-hour clock, from an epoch ms, an ISO string or a Date.
|
|
276
|
+
export function hhmm(t) {
|
|
277
|
+
const d = t instanceof Date ? t : new Date(typeof t === "number" ? t : Date.parse(t));
|
|
278
|
+
if (isNaN(d.getTime())) return "";
|
|
279
|
+
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function ago(ms) {
|
|
283
|
+
const m = Math.floor(Math.max(0, ms) / 60000);
|
|
284
|
+
if (m < 1) return "just now";
|
|
285
|
+
if (m < 60) return m + " min ago";
|
|
286
|
+
const h = Math.floor(m / 60);
|
|
287
|
+
return h + (h === 1 ? " hour ago" : " hours ago");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// How long the revising strip waits without a change by the agent before it
|
|
291
|
+
// says the agent may have stopped.
|
|
292
|
+
export const STALL_MS = 5 * 60 * 1000;
|
|
293
|
+
|
|
294
|
+
// The revising strip's facts. `since` (epoch ms) is when the agent got the
|
|
295
|
+
// answer; `activityAt` (ISO, heartbeat) is its newest change to the deck. A
|
|
296
|
+
// change from BEFORE the answer is not revising, so it does not count.
|
|
297
|
+
export function revisingStatus({ since, activityAt, now }) {
|
|
298
|
+
const act = activityAt ? Date.parse(activityAt) : NaN;
|
|
299
|
+
const lastChange = !isNaN(act) && (!since || act >= since) ? act : null;
|
|
300
|
+
const last = lastChange || since || now;
|
|
301
|
+
return { lastChange, stalled: now - last > STALL_MS };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// One chrome instance per page. `opts`:
|
|
305
|
+
// CFG — the page's injected config (embedded / approved / asked /
|
|
306
|
+
// heartbeat / subjectHash / logoMark are read here)
|
|
307
|
+
// session — the page's gate session (gate-session.js): the decision
|
|
308
|
+
// wire, the heartbeat reader, the next-load note
|
|
309
|
+
// what — { revising: "the plan", approved: "Plan", answer: … }
|
|
310
|
+
// isWorking() — the overlay's mode flag
|
|
311
|
+
// workingNote() — the overlay's current working headline (may be null)
|
|
312
|
+
// onOffset(h) — called with the total height of the strips under the bar
|
|
313
|
+
// onSaveCtx() — the overlay persists its round context (the shell's
|
|
314
|
+
// hand-over state rides in it — see ctxFields)
|
|
315
|
+
// onChange() — the overlay refreshes its buttons (a decision started or
|
|
316
|
+
// ended)
|
|
317
|
+
// onDecided(decision, ack, body) — a decision was saved: mark rows sent etc.
|
|
318
|
+
// onDecisionFailed(outcome) — it was not: the overlay re-arms
|
|
319
|
+
// onAlreadyAnswered(answered) — another window answered this already
|
|
320
|
+
// onApprovalPickedUp(epochMs) — the agent received a saved approval
|
|
321
|
+
// onLost() — standalone: the gate command is gone
|
|
322
|
+
// reloadBlocker() — null | "typing" | "edits": why a newer version must not
|
|
323
|
+
// replace the page right now
|
|
324
|
+
// reloadIntoNewRound() — persist and reload into the newer version
|
|
325
|
+
// staleLine — preview gate only: the one line shown while the preview
|
|
326
|
+
// on screen is older than the slides (CFG / heartbeat
|
|
327
|
+
// `previewStale`); Approve is muted meanwhile
|
|
328
|
+
export function createGateShell(opts) {
|
|
329
|
+
const CFG = opts.CFG || {};
|
|
330
|
+
const SES = opts.session || null;
|
|
331
|
+
const what = opts.what || { revising: "the slides", approved: "Slides" };
|
|
332
|
+
const isWorking = opts.isWorking || (() => false);
|
|
333
|
+
const workingNote = opts.workingNote || (() => null);
|
|
334
|
+
const onOffset = opts.onOffset || (() => {});
|
|
335
|
+
const onSaveCtx = opts.onSaveCtx || (() => {});
|
|
336
|
+
const onChange = opts.onChange || (() => {});
|
|
337
|
+
const reloadBlocker = opts.reloadBlocker || (() => null);
|
|
338
|
+
const reloadIntoNewRound = opts.reloadIntoNewRound || (() => location.reload());
|
|
339
|
+
const call = (name, ...args) => (typeof opts[name] === "function" ? opts[name](...args) : undefined);
|
|
340
|
+
const now = () => Date.now();
|
|
341
|
+
|
|
342
|
+
// The decision + the workspace hand-over. Everything but `activityAt` (a live
|
|
343
|
+
// heartbeat fact) is persisted in the round ctx.
|
|
344
|
+
const state = {
|
|
345
|
+
agentPrompt: null, // ack.message — what the user pastes into their chat with the agent
|
|
346
|
+
phase: null, // null | "handoff" | "revising" | "answered" | "approved"
|
|
347
|
+
decision: null, // "approve" | "comments" — what the user answered
|
|
348
|
+
task: null, // the agent's request the answer went to (ack.task)
|
|
349
|
+
decidedAt: null, // epoch ms the answer was saved
|
|
350
|
+
pickedUpAt: null, // epoch ms the agent received the answer
|
|
351
|
+
revisingSince: null, // epoch ms the agent has had the comments
|
|
352
|
+
handoffOpen: false, // the hand-off dialog is up (it survives a refresh)
|
|
353
|
+
answered: null, // { action, at } — answered in another window
|
|
354
|
+
pending: null, // the decision on its way: { operationId, decision, body, startedAt }
|
|
355
|
+
activityAt: null, // heartbeat: the newest change the agent made (ISO)
|
|
356
|
+
};
|
|
357
|
+
let bar = null;
|
|
358
|
+
let growEl = null;
|
|
359
|
+
let infoBtn = null;
|
|
360
|
+
let waiterEl = null;
|
|
361
|
+
let conn = "unknown"; // unknown | online | offline | signedout — the heartbeat's verdict
|
|
362
|
+
let monitorTimer = null;
|
|
363
|
+
let reloading = false;
|
|
364
|
+
let restoredPending = null;
|
|
365
|
+
let lastStripKey = null;
|
|
366
|
+
let authFromDecision = false; // a refused ANSWER stays on screen until it is retried
|
|
367
|
+
// The preview on screen predates the agent's latest slide changes: an approval
|
|
368
|
+
// would cover something the reviewer has not seen. Comments stay allowed.
|
|
369
|
+
let previewStale = !!CFG.previewStale;
|
|
370
|
+
const notices = {}; // id → { node, key }
|
|
371
|
+
// Hosted: the workspace page frames this gate and shows its MESSAGES from what
|
|
372
|
+
// this page publishes — the status line, the notices and banners, the hand-off
|
|
373
|
+
// dialog. The bar (brand, title, the overlay's notes) stays the gate's; the
|
|
374
|
+
// strips stay built, since the overlays measure them, but hidden.
|
|
375
|
+
const hosted = !!CFG.embedded && typeof window !== "undefined" && window.parent !== window;
|
|
376
|
+
const noticeModels = {}; // id → the notice as shown, for the host
|
|
377
|
+
const banners = []; // the not-asked / approved banners, in order
|
|
378
|
+
const chromeActions = {}; // host action id → handler
|
|
379
|
+
let waiterModel = null; // { cls, text, detail, spin } — the page's status badge
|
|
380
|
+
let notAskedNote = ""; // the badge's tooltip while the agent has not asked yet
|
|
381
|
+
let approvedNote = ""; // ...and once this content is approved and nobody asks again
|
|
382
|
+
let toastSeq = 0;
|
|
383
|
+
let lastChromeKey = null;
|
|
384
|
+
|
|
385
|
+
function logoNode(cls, idSuffix) {
|
|
386
|
+
const wrap = el("div", cls || "sm-logo");
|
|
387
|
+
wrap.appendChild(el("div", "sm-logo-svg", logoWithIds(idSuffix || "")));
|
|
388
|
+
if (CFG.logoMark) {
|
|
389
|
+
const img = el("img", "sm-logo-img");
|
|
390
|
+
img.src = CFG.logoMark;
|
|
391
|
+
img.alt = "";
|
|
392
|
+
img.onerror = () => img.remove();
|
|
393
|
+
wrap.appendChild(img);
|
|
394
|
+
}
|
|
395
|
+
return wrap;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function mkButton(label, variant, iconName) {
|
|
399
|
+
const b = el("button", "sm-btn sm-btn-" + variant, (iconName ? ICONS[iconName] : "") + "<span></span>");
|
|
400
|
+
b.type = "button";
|
|
401
|
+
b.querySelector("span").textContent = label;
|
|
402
|
+
return b;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function injectAssets(css) {
|
|
406
|
+
const style = el("style");
|
|
407
|
+
style.id = "sm-gate-style";
|
|
408
|
+
style.textContent = SHELL_CSS + (css || "");
|
|
409
|
+
document.head.appendChild(style);
|
|
410
|
+
const pre1 = el("link");
|
|
411
|
+
pre1.rel = "preconnect";
|
|
412
|
+
pre1.href = "https://fonts.googleapis.com";
|
|
413
|
+
const pre2 = el("link");
|
|
414
|
+
pre2.rel = "preconnect";
|
|
415
|
+
pre2.href = "https://fonts.gstatic.com";
|
|
416
|
+
pre2.crossOrigin = "anonymous";
|
|
417
|
+
const fonts = el("link");
|
|
418
|
+
fonts.rel = "stylesheet";
|
|
419
|
+
fonts.href = "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
|
|
420
|
+
document.head.appendChild(pre1);
|
|
421
|
+
document.head.appendChild(pre2);
|
|
422
|
+
document.head.appendChild(fonts);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// The top bar: brand + title/hint + a flexible middle the overlay fills
|
|
426
|
+
// (`bar.grow` is the slot before the right-hand controls).
|
|
427
|
+
function buildBar(title, hint) {
|
|
428
|
+
bar = el("div", "sm-o sm-bar");
|
|
429
|
+
bar.setAttribute("data-sm-overlay", "1");
|
|
430
|
+
const brand = el("div", "sm-brand");
|
|
431
|
+
brand.appendChild(logoNode("sm-logo"));
|
|
432
|
+
brand.appendChild(el("div", "sm-word", wordmarkHtml()));
|
|
433
|
+
bar.appendChild(brand);
|
|
434
|
+
bar.appendChild(el("div", "sm-sep"));
|
|
435
|
+
const meta = el("div", "sm-meta");
|
|
436
|
+
const titleEl = el("div", "t");
|
|
437
|
+
titleEl.appendChild(document.createTextNode(title));
|
|
438
|
+
// The notes in the middle of the bar fold into this (i) when there is no
|
|
439
|
+
// room for them — squeezed into a one-word column they read as noise.
|
|
440
|
+
infoBtn = el("button", "sm-info", "i");
|
|
441
|
+
infoBtn.type = "button";
|
|
442
|
+
infoBtn.title = "About this page";
|
|
443
|
+
infoBtn.setAttribute("aria-label", "About this page");
|
|
444
|
+
infoBtn.onclick = (e) => {
|
|
445
|
+
e.stopPropagation();
|
|
446
|
+
toggleInfo();
|
|
447
|
+
};
|
|
448
|
+
titleEl.appendChild(infoBtn);
|
|
449
|
+
meta.appendChild(titleEl);
|
|
450
|
+
const hintEl = el("div", "h");
|
|
451
|
+
hintEl.textContent = hint || "";
|
|
452
|
+
meta.appendChild(hintEl);
|
|
453
|
+
bar.appendChild(meta);
|
|
454
|
+
growEl = el("div", "sm-grow");
|
|
455
|
+
bar.appendChild(growEl);
|
|
456
|
+
if (CFG.embedded) {
|
|
457
|
+
// Workspace only: the page's one status badge (ready for review, paste
|
|
458
|
+
// the message, picked up, approved), refreshed with every heartbeat.
|
|
459
|
+
waiterEl = el("div", "sm-waiter");
|
|
460
|
+
waiterEl.style.display = "none";
|
|
461
|
+
bar.appendChild(waiterEl);
|
|
462
|
+
}
|
|
463
|
+
document.body.appendChild(bar);
|
|
464
|
+
if (hosted) document.documentElement.classList.add("sm-hosted");
|
|
465
|
+
// The chrome height is measured, so it is re-measured whenever it can have
|
|
466
|
+
// changed: a frame that loaded hidden (0 px) or narrow (strips wrapped tall)
|
|
467
|
+
// and is shown later, a resize, a strip that rewraps. Measured once, the
|
|
468
|
+
// surface sat under the strips or far below them until the next heartbeat —
|
|
469
|
+
// and while the workspace is offline there is none.
|
|
470
|
+
window.addEventListener("resize", offsetSurface);
|
|
471
|
+
watchSize(bar, offsetSurface);
|
|
472
|
+
watchSize(growEl, fitBar);
|
|
473
|
+
setTimeout(offsetSurface, 0);
|
|
474
|
+
if (CFG.embedded) startMonitor();
|
|
475
|
+
return { bar, titleEl, hintEl, grow: growEl };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// The middle of the bar is what is left after brand, title and controls. Under
|
|
479
|
+
// ~300px a note there is unreadable: the bar goes "tight" and the notes move
|
|
480
|
+
// behind the (i).
|
|
481
|
+
function fitBar() {
|
|
482
|
+
if (!bar || !growEl) return;
|
|
483
|
+
const tight = growEl.clientWidth < 300;
|
|
484
|
+
bar.classList.toggle("is-tight", tight);
|
|
485
|
+
if (infoBtn) infoBtn.classList.toggle("has", tight && infoNotes().length > 0);
|
|
486
|
+
}
|
|
487
|
+
function infoNotes() {
|
|
488
|
+
if (!growEl) return [];
|
|
489
|
+
return Array.prototype.filter.call(growEl.querySelectorAll(".sm-note"), (n) => !(n.classList.contains("sm-chart-note") && !n.classList.contains("on")));
|
|
490
|
+
}
|
|
491
|
+
function toggleInfo() {
|
|
492
|
+
const old = document.getElementById("sm-info-pop");
|
|
493
|
+
if (old) {
|
|
494
|
+
old.remove();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const pop = el("div", "sm-o sm-info-pop");
|
|
498
|
+
pop.id = "sm-info-pop";
|
|
499
|
+
pop.setAttribute("data-sm-overlay", "1");
|
|
500
|
+
infoNotes().forEach((n) => pop.appendChild(el("p", null, n.innerHTML)));
|
|
501
|
+
const r = infoBtn.getBoundingClientRect();
|
|
502
|
+
pop.style.top = r.bottom + 10 + "px";
|
|
503
|
+
pop.style.left = Math.max(12, Math.min(r.left - 12, window.innerWidth - 392)) + "px";
|
|
504
|
+
document.body.appendChild(pop);
|
|
505
|
+
const close = (e) => {
|
|
506
|
+
if (e && pop.contains(e.target)) return;
|
|
507
|
+
pop.remove();
|
|
508
|
+
document.removeEventListener("pointerdown", close, true);
|
|
509
|
+
};
|
|
510
|
+
setTimeout(() => document.addEventListener("pointerdown", close, true), 0);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ---------- the heartbeat monitor (workspace page only) ----------
|
|
514
|
+
// One poll every 2 s for the page's whole life — it never times out (the
|
|
515
|
+
// workspace page has no "session ended"). It drives the waiter chip, the
|
|
516
|
+
// offline / signed-out states, the hand-over (picked up? still revising?) and
|
|
517
|
+
// the round: a newer version reloads the page, keeping everything unsent.
|
|
518
|
+
function startMonitor() {
|
|
519
|
+
if (!CFG.heartbeat || monitorTimer) return;
|
|
520
|
+
tick();
|
|
521
|
+
monitorTimer = setInterval(tick, 2000);
|
|
522
|
+
}
|
|
523
|
+
function tick() {
|
|
524
|
+
let p;
|
|
525
|
+
try {
|
|
526
|
+
p = fetch(CFG.heartbeat, { cache: "no-store", credentials: "same-origin" });
|
|
527
|
+
} catch (e) {
|
|
528
|
+
p = Promise.reject(e);
|
|
529
|
+
}
|
|
530
|
+
Promise.resolve(p)
|
|
531
|
+
.then(
|
|
532
|
+
(r) =>
|
|
533
|
+
r
|
|
534
|
+
.json()
|
|
535
|
+
.catch(() => null)
|
|
536
|
+
.then((b) => SES.readBeat(r.status, b, opts.service)),
|
|
537
|
+
() => ({ conn: "offline", beat: null }),
|
|
538
|
+
)
|
|
539
|
+
.then(onBeat, () => {});
|
|
540
|
+
}
|
|
541
|
+
function onBeat(r) {
|
|
542
|
+
const was = conn;
|
|
543
|
+
conn = r.conn;
|
|
544
|
+
if (conn === "signedout") showNotice("auth", authNotice());
|
|
545
|
+
else if (!authFromDecision) clearNotice("auth");
|
|
546
|
+
if (conn !== "online") {
|
|
547
|
+
renderWaiter();
|
|
548
|
+
renderStale();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// The workspace answers again: an answer that waited for it goes now.
|
|
552
|
+
if (was === "offline" || was === "signedout") SES.retryDecision();
|
|
553
|
+
const beat = r.beat;
|
|
554
|
+
if (beat.activityAt) state.activityAt = beat.activityAt;
|
|
555
|
+
if (typeof beat.previewStale === "boolean") setPreviewStale(beat.previewStale);
|
|
556
|
+
else renderStale();
|
|
557
|
+
// the agent received the answer (the user pasted the message, the agent ran its command)
|
|
558
|
+
if (state.phase === "handoff" && state.task) {
|
|
559
|
+
const at = SES.pickedUpAt(beat, state.task);
|
|
560
|
+
if (at) pickedUp(at);
|
|
561
|
+
}
|
|
562
|
+
// answered in another window while this one still offers the choice
|
|
563
|
+
if (CFG.asked && !state.phase && !state.pending && !isWorking() && beat.answered && beat.answered.taskId === CFG.asked) {
|
|
564
|
+
answeredElsewhere({ action: beat.answered.action, at: beat.answered.at });
|
|
565
|
+
}
|
|
566
|
+
if (beat.round && CFG.round && beat.round !== CFG.round) roundMoved(beat);
|
|
567
|
+
renderWaiter();
|
|
568
|
+
renderRevising();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// A newer version (or a new request from the agent) is on the server.
|
|
572
|
+
function roundMoved(beat) {
|
|
573
|
+
if (reloading || state.pending || (SES && SES.decisionPending())) return; // mid-submit: its answer decides
|
|
574
|
+
// A hand-over in progress stays on screen: the user's OWN answer can move the
|
|
575
|
+
// round (an approve that commits inline edits changes the content; one the
|
|
576
|
+
// agent never asked for creates its request). The pickup ends the hand-over;
|
|
577
|
+
// a newer version loads after that.
|
|
578
|
+
if (state.phase === "handoff") return;
|
|
579
|
+
// the veil after an approval stays up until the agent asks for something new
|
|
580
|
+
if ((state.phase === "approved" || state.phase === "answered") && !beat.taskId) return;
|
|
581
|
+
const blocker = reloadBlocker();
|
|
582
|
+
if (blocker) {
|
|
583
|
+
showNotice("newer", newerNotice(blocker));
|
|
584
|
+
return; // "typing" reloads on a later tick, "edits" only on the button
|
|
585
|
+
}
|
|
586
|
+
reload();
|
|
587
|
+
}
|
|
588
|
+
function reload() {
|
|
589
|
+
if (reloading) return;
|
|
590
|
+
reloading = true;
|
|
591
|
+
if (monitorTimer) clearInterval(monitorTimer);
|
|
592
|
+
reloadIntoNewRound();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function authNotice() {
|
|
596
|
+
return { tone: "error", title: "Your workspace signed you out", text: "Ask your agent to open the slidemind workspace. What you wrote here is kept." };
|
|
597
|
+
}
|
|
598
|
+
function newerNotice(blocker) {
|
|
599
|
+
return blocker === "edits"
|
|
600
|
+
? {
|
|
601
|
+
tone: "info",
|
|
602
|
+
title: "A newer version is ready",
|
|
603
|
+
text: "Your agent changed " + what.revising + ". Your edits here are not sent — reloading shows the new version without them.",
|
|
604
|
+
actions: [{ label: "Reload", primary: true, onClick: reload }],
|
|
605
|
+
}
|
|
606
|
+
: {
|
|
607
|
+
tone: "info",
|
|
608
|
+
title: "A newer version is ready",
|
|
609
|
+
text: "It loads as soon as you finish what you are writing — nothing you wrote is lost.",
|
|
610
|
+
actions: [{ label: "Reload now", onClick: reload }],
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ---------- the waiter chip ----------
|
|
615
|
+
// The page's one status badge. A STATE (approved, still being written, the
|
|
616
|
+
// agent revising, waiting, away) is said here in a few words, its why in the
|
|
617
|
+
// tooltip — never as a banner; a banner is for something the user must do.
|
|
618
|
+
function renderWaiter() {
|
|
619
|
+
let cls = "";
|
|
620
|
+
let text = null;
|
|
621
|
+
let detail = "";
|
|
622
|
+
let spin = false;
|
|
623
|
+
let action = null; // a click on the badge runs this chrome action
|
|
624
|
+
if (conn === "offline") {
|
|
625
|
+
cls = "warn";
|
|
626
|
+
text = "Reconnecting…";
|
|
627
|
+
detail = "Lost the connection to the workspace. What you wrote is kept.";
|
|
628
|
+
} else if (conn === "signedout") {
|
|
629
|
+
cls = "err";
|
|
630
|
+
text = "Signed out";
|
|
631
|
+
} else if (conn === "unknown" || state.phase === "answered") {
|
|
632
|
+
text = null; // nothing proven yet / nothing left to hand over
|
|
633
|
+
} else if (alreadyApproved()) {
|
|
634
|
+
cls = "on";
|
|
635
|
+
text = reviewNotNeeded() ? "No review needed" : "Approved" + (approvedDay() ? " · " + approvedDay() : "");
|
|
636
|
+
detail = reviewNotNeeded() ? approvedNote : [what.approved + " approved " + approvedLabel() + ".", approvedNote].filter(Boolean).join(" ");
|
|
637
|
+
} else if (state.phase === "approved") {
|
|
638
|
+
cls = "on";
|
|
639
|
+
text = "Approved";
|
|
640
|
+
detail = "Your agent has your approval and continues.";
|
|
641
|
+
} else if (state.phase === "revising") {
|
|
642
|
+
const s = revisingStatus({ since: state.revisingSince, activityAt: state.activityAt, now: now() });
|
|
643
|
+
cls = s.stalled ? "warn" : "on";
|
|
644
|
+
spin = !s.stalled;
|
|
645
|
+
text = s.stalled ? "Your agent seems to have stopped" : "Your agent is working on your comments";
|
|
646
|
+
detail = s.lastChange ? "Last change at " + hhmm(s.lastChange) + ". This page updates by itself." : "This page updates by itself.";
|
|
647
|
+
} else if (state.phase === "handoff") {
|
|
648
|
+
// The STATE only. What to do about it was said once, in the dialog right
|
|
649
|
+
// after the click — no second strip repeating it. A click on the badge
|
|
650
|
+
// brings that dialog (and the message) back.
|
|
651
|
+
text = "Answer saved";
|
|
652
|
+
detail = "Click to see the message to paste into your agent chat.";
|
|
653
|
+
action = "handoff-open";
|
|
654
|
+
} else if (isWorking()) {
|
|
655
|
+
cls = "on";
|
|
656
|
+
spin = true;
|
|
657
|
+
text = "Your agent is working on it";
|
|
658
|
+
detail = workingNote() || "This page updates by itself.";
|
|
659
|
+
} else if (notAskedYet()) {
|
|
660
|
+
text = "Not ready for review yet";
|
|
661
|
+
detail = notAskedNote || "Your agent is still working on " + what.revising + ". This page updates by itself when it asks you.";
|
|
662
|
+
} else if (CFG.asked) {
|
|
663
|
+
cls = "on";
|
|
664
|
+
text = "Ready for your review";
|
|
665
|
+
detail = "When you are done, you get a message to paste into your agent chat.";
|
|
666
|
+
}
|
|
667
|
+
waiterModel = text ? { cls, text, detail, spin, action } : null;
|
|
668
|
+
if (!waiterEl) return publishChrome();
|
|
669
|
+
if (!text) {
|
|
670
|
+
waiterEl.style.display = "none";
|
|
671
|
+
publishChrome();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
waiterEl.style.display = "";
|
|
675
|
+
waiterEl.className = "sm-waiter" + (cls ? " " + cls : "") + (action ? " act" : "");
|
|
676
|
+
waiterEl.title = detail;
|
|
677
|
+
waiterEl.onclick = action ? () => chromeActions[action] && chromeActions[action]() : null;
|
|
678
|
+
if (waiterEl.textContent !== text) {
|
|
679
|
+
waiterEl.innerHTML = '<span class="dot"></span><span></span>';
|
|
680
|
+
waiterEl.lastChild.textContent = text;
|
|
681
|
+
}
|
|
682
|
+
publishChrome();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Has the agent asked for this review yet? (workspace: a pending task)
|
|
686
|
+
function notAskedYet() {
|
|
687
|
+
return !!CFG.embedded && !CFG.asked && !CFG.approved && !isWorking();
|
|
688
|
+
}
|
|
689
|
+
// This exact content is approved and nobody asks again: nothing to do here.
|
|
690
|
+
function alreadyApproved() {
|
|
691
|
+
return !!CFG.embedded && !!CFG.approved && !CFG.asked && !isWorking();
|
|
692
|
+
}
|
|
693
|
+
// Approved before, and the agent asks again (it wants the answer for ITS
|
|
694
|
+
// request): Approve stays available — "approved" next to "your turn" is the
|
|
695
|
+
// contradiction this replaces. A deck that never needed a review is not
|
|
696
|
+
// "approved before": asked, it is an ordinary question.
|
|
697
|
+
function askedAgain() {
|
|
698
|
+
return !!CFG.embedded && !!CFG.approved && !!CFG.asked && !isWorking() && !reviewNotNeeded();
|
|
699
|
+
}
|
|
700
|
+
// Adopted from the user's own PowerPoint file: settled like an approved deck,
|
|
701
|
+
// but nobody approved anything, so it is never worded as an approval.
|
|
702
|
+
function reviewNotNeeded() {
|
|
703
|
+
return !!(CFG.approved && CFG.approved.notNeeded);
|
|
704
|
+
}
|
|
705
|
+
function decisionPending() {
|
|
706
|
+
return !!state.pending;
|
|
707
|
+
}
|
|
708
|
+
function isPreviewStale() {
|
|
709
|
+
return previewStale;
|
|
710
|
+
}
|
|
711
|
+
function setPreviewStale(stale) {
|
|
712
|
+
const changed = stale !== previewStale;
|
|
713
|
+
previewStale = stale;
|
|
714
|
+
renderStale();
|
|
715
|
+
if (changed) onChange();
|
|
716
|
+
}
|
|
717
|
+
// One plain line while it lasts; while the agent revises THIS page's comments
|
|
718
|
+
// the revising strip already says the slides are changing. Before the first
|
|
719
|
+
// beat nothing is claimed.
|
|
720
|
+
function renderStale() {
|
|
721
|
+
if (!previewStale || !opts.staleLine || isWorking() || state.phase || conn === "unknown") return clearNotice("stale");
|
|
722
|
+
if (opts.staleLineIdle) showNotice("stale", Object.assign({ tone: "info" }, opts.staleLineIdle));
|
|
723
|
+
else showNotice("stale", { tone: "info", title: opts.staleLine });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// The agent asked again on the same bytes: nothing was worked in.
|
|
727
|
+
function showAskedAgainUnchanged() {
|
|
728
|
+
showNotice("again", {
|
|
729
|
+
tone: "info",
|
|
730
|
+
title: "Nothing changed since your last answer",
|
|
731
|
+
text: "If something still needs to change, say so in a comment.",
|
|
732
|
+
dismiss: true,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
// Is this row part of the answer on its way? It is frozen until the server
|
|
736
|
+
// answers — editing or deleting it then would show text the agent never got.
|
|
737
|
+
function inFlight(id) {
|
|
738
|
+
const p = state.pending;
|
|
739
|
+
return !!(p && p.body && (p.body.comments || []).some((w) => w && w.id === id));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ---------- the strips under the bar ----------
|
|
743
|
+
function watchSize(node, fn) {
|
|
744
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(() => fn()).observe(node);
|
|
745
|
+
}
|
|
746
|
+
function stripHost() {
|
|
747
|
+
let host = document.getElementById("sm-strips");
|
|
748
|
+
if (!host) {
|
|
749
|
+
host = el("div", "sm-o sm-strips");
|
|
750
|
+
host.id = "sm-strips";
|
|
751
|
+
host.setAttribute("data-sm-overlay", "1");
|
|
752
|
+
document.body.appendChild(host);
|
|
753
|
+
watchSize(host, offsetSurface);
|
|
754
|
+
}
|
|
755
|
+
return host;
|
|
756
|
+
}
|
|
757
|
+
// The strips make the chrome taller; the overlay refits its surface (rail +
|
|
758
|
+
// stage) under the total, so nothing is painted over them.
|
|
759
|
+
function offsetSurface() {
|
|
760
|
+
const host = document.getElementById("sm-strips");
|
|
761
|
+
const h = host ? host.offsetHeight : 0;
|
|
762
|
+
const total = (bar ? bar.offsetHeight || 62 : 62) + h; // hosted: the strips are hidden, so h is 0
|
|
763
|
+
document.documentElement.style.setProperty("--deck-chrome-h", total + "px");
|
|
764
|
+
onOffset(total);
|
|
765
|
+
fitBar();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// A notice: one line of what happened + at most one thing to do. `n`:
|
|
769
|
+
// { tone: info|warn|error|ok, title, text, spin, actions: [{label, primary, onClick}], dismiss }.
|
|
770
|
+
// Re-showing the same notice is free (the heartbeat re-asserts some every 2 s).
|
|
771
|
+
function showNotice(id, n) {
|
|
772
|
+
const key = JSON.stringify([n.tone, n.title, n.text, n.spin, (n.actions || []).map((a) => a.label), n.dismiss]);
|
|
773
|
+
const have = notices[id];
|
|
774
|
+
if (have && have.key === key && have.node.parentNode) return;
|
|
775
|
+
const node = have && have.node.parentNode ? have.node : el("div");
|
|
776
|
+
node.className = "sm-notice sm-notice--" + (n.tone || "info");
|
|
777
|
+
node.setAttribute("data-notice", id);
|
|
778
|
+
node.innerHTML =
|
|
779
|
+
(n.spin ? '<div class="sm-spin sm-spin-lg"></div>' : n.tone === "ok" ? ICONS.check : n.tone === "error" || n.tone === "warn" ? ICONS.message : ICONS.sparkles) +
|
|
780
|
+
'<div class="txt"><b></b><span></span></div><div class="acts"></div>';
|
|
781
|
+
node.querySelector(".txt b").textContent = n.title || "";
|
|
782
|
+
node.querySelector(".txt span").textContent = n.text || "";
|
|
783
|
+
const acts = node.querySelector(".acts");
|
|
784
|
+
(n.actions || []).forEach((a) => {
|
|
785
|
+
const b = el("button", "sm-btn " + (a.primary ? "sm-btn-primary" : "sm-btn-ghost"), "<span></span>");
|
|
786
|
+
b.type = "button";
|
|
787
|
+
b.querySelector("span").textContent = a.label;
|
|
788
|
+
b.onclick = a.onClick;
|
|
789
|
+
acts.appendChild(b);
|
|
790
|
+
});
|
|
791
|
+
if (n.copy) appendCopy(acts);
|
|
792
|
+
if (n.dismiss) {
|
|
793
|
+
const x = el("button", "sm-notice-x", ICONS.x);
|
|
794
|
+
x.type = "button";
|
|
795
|
+
x.title = "Dismiss";
|
|
796
|
+
x.setAttribute("aria-label", "Dismiss");
|
|
797
|
+
x.onclick = () => clearNotice(id);
|
|
798
|
+
acts.appendChild(x);
|
|
799
|
+
}
|
|
800
|
+
if (!node.parentNode) stripHost().appendChild(node);
|
|
801
|
+
notices[id] = { node, key };
|
|
802
|
+
noticeModels[id] = n;
|
|
803
|
+
offsetSurface();
|
|
804
|
+
publishChrome();
|
|
805
|
+
}
|
|
806
|
+
function clearNotice(id) {
|
|
807
|
+
const have = notices[id];
|
|
808
|
+
if (!have) return;
|
|
809
|
+
delete notices[id];
|
|
810
|
+
delete noticeModels[id];
|
|
811
|
+
if (have.node.parentNode) have.node.parentNode.removeChild(have.node);
|
|
812
|
+
offsetSurface();
|
|
813
|
+
publishChrome();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Not asked yet / approved and done are STATES: standalone they are a strip
|
|
817
|
+
// under the bar, hosted they are the status badge (renderWaiter) and never a
|
|
818
|
+
// banner (chromeModel leaves them out).
|
|
819
|
+
function showNotAskedBanner(text) {
|
|
820
|
+
notAskedNote = text;
|
|
821
|
+
const node = el("div", "sm-o sm-approved-banner sm-notasked");
|
|
822
|
+
node.setAttribute("data-sm-overlay", "1");
|
|
823
|
+
node.innerHTML = ICONS.send + '<div class="txt"><b>Not ready for review yet</b><span></span></div>';
|
|
824
|
+
node.querySelector("span").textContent = text;
|
|
825
|
+
stripHost().appendChild(node);
|
|
826
|
+
banners.push({ id: "not-asked", tone: "info", title: "Not ready for review yet", text });
|
|
827
|
+
offsetSurface();
|
|
828
|
+
renderWaiter();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function approvedWhen() {
|
|
832
|
+
const at = (CFG.approved || {}).at;
|
|
833
|
+
const when = at ? new Date(at) : null;
|
|
834
|
+
return when && !isNaN(when.getTime()) ? when : null;
|
|
835
|
+
}
|
|
836
|
+
// "6 Oct" — the badge's date
|
|
837
|
+
function approvedDay() {
|
|
838
|
+
const when = approvedWhen();
|
|
839
|
+
return when ? when.toLocaleDateString(undefined, { day: "numeric", month: "short" }) : "";
|
|
840
|
+
}
|
|
841
|
+
function approvedLabel() {
|
|
842
|
+
const a = CFG.approved || {};
|
|
843
|
+
const when = approvedWhen();
|
|
844
|
+
const t = when ? approvedDay() + ", " + hhmm(when) : "";
|
|
845
|
+
return (t ? "on " + t : "") + (a.by ? (t ? " by " : "by ") + a.by : "");
|
|
846
|
+
}
|
|
847
|
+
// Approved and done (`askTitle` absent): the badge's tooltip, a strip only
|
|
848
|
+
// standalone. Approved and asked AGAIN (`askTitle`): the user has to act, so
|
|
849
|
+
// it is a notice, titled with what is asked.
|
|
850
|
+
function showApprovedBanner(text, askTitle) {
|
|
851
|
+
if (!askTitle) approvedNote = text;
|
|
852
|
+
const title = askTitle || (reviewNotNeeded() ? "No review needed" : what.approved + " approved " + approvedLabel());
|
|
853
|
+
const body = askTitle ? "You approved it " + approvedLabel() + ". " + text : text;
|
|
854
|
+
const node = el("div", "sm-o sm-approved-banner");
|
|
855
|
+
node.setAttribute("data-sm-overlay", "1");
|
|
856
|
+
node.innerHTML = ICONS.check + '<div class="txt"><b></b><span></span></div>';
|
|
857
|
+
node.querySelector("b").textContent = title;
|
|
858
|
+
node.querySelector("span").textContent = body;
|
|
859
|
+
stripHost().appendChild(node);
|
|
860
|
+
banners.push({ id: askTitle ? "asked-again" : "approved", tone: askTitle ? "info" : "ok", title, text: body });
|
|
861
|
+
offsetSurface();
|
|
862
|
+
renderWaiter();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Copy the message that brings the agent back (the server's words: the deck,
|
|
866
|
+
// the answer and the command that receives it). `done(ok)` says whether a copy
|
|
867
|
+
// path REALLY succeeded — the clipboard API first, a selected textarea second
|
|
868
|
+
// (no permission needed; it works in Codex's own browser pane too). The page
|
|
869
|
+
// never says "Copied" for a copy that did not happen.
|
|
870
|
+
function copyText(text, done) {
|
|
871
|
+
const fallback = () => {
|
|
872
|
+
let ok = false;
|
|
873
|
+
try {
|
|
874
|
+
const ta = el("textarea");
|
|
875
|
+
ta.value = text;
|
|
876
|
+
ta.setAttribute("readonly", "");
|
|
877
|
+
ta.style.cssText = "position:fixed;top:-1000px;opacity:0;";
|
|
878
|
+
document.body.appendChild(ta);
|
|
879
|
+
ta.select();
|
|
880
|
+
ok = document.execCommand("copy") === true;
|
|
881
|
+
ta.remove();
|
|
882
|
+
} catch {
|
|
883
|
+
ok = false;
|
|
884
|
+
}
|
|
885
|
+
done(ok);
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
navigator.clipboard.writeText(text).then(() => done(true), fallback);
|
|
889
|
+
} catch {
|
|
890
|
+
fallback();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function copyMessageButton(cls) {
|
|
894
|
+
const b = el("button", cls || "sm-copy-msg", ICONS.copy + "<span>Copy message</span>");
|
|
895
|
+
b.type = "button";
|
|
896
|
+
b.onclick = () => {
|
|
897
|
+
copyText(state.agentPrompt || "", (ok) => {
|
|
898
|
+
b.querySelector("span").textContent = ok ? "Copied" : "Select it and press ⌘C";
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
return b;
|
|
902
|
+
}
|
|
903
|
+
function appendCopy(parent, cls) {
|
|
904
|
+
if (state.agentPrompt) parent.appendChild(copyMessageButton(cls));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// The hand-over strip under the bar: what happened to the answer, the one
|
|
908
|
+
// thing to do (if any) and a copy button. While the agent revises it shows the
|
|
909
|
+
// agent's own last change (heartbeat activityAt); quiet for 5 minutes, it says
|
|
910
|
+
// the agent may have stopped. Re-rendered on every heartbeat, rebuilt only
|
|
911
|
+
// when its words change.
|
|
912
|
+
function stripModel() {
|
|
913
|
+
const t = now();
|
|
914
|
+
const approve = state.decision === "approve";
|
|
915
|
+
switch (state.phase) {
|
|
916
|
+
case "handoff":
|
|
917
|
+
// no warning: the dialog after the click already said "paste it", once
|
|
918
|
+
return {
|
|
919
|
+
icon: ICONS.check,
|
|
920
|
+
title: approve ? "Approval saved" : "Comments saved",
|
|
921
|
+
text: "Paste the message into your agent chat.",
|
|
922
|
+
copy: true,
|
|
923
|
+
};
|
|
924
|
+
case "answered":
|
|
925
|
+
return { tone: "ok", icon: ICONS.check, title: "Already answered — approved" + (state.answered && state.answered.at ? " at " + hhmm(state.answered.at) : ""), text: "This was answered in another window. Nothing else to do here." };
|
|
926
|
+
case "revising": {
|
|
927
|
+
const s = revisingStatus({ since: state.revisingSince, activityAt: state.activityAt, now: t });
|
|
928
|
+
// it HAS the comments (the receipt landed) — the message would only ask again; a word to the agent is enough
|
|
929
|
+
if (s.stalled) return { tone: "warn", icon: ICONS.send, title: "Your agent seems to have stopped", text: "Go back to your agent and say:", say: "continue" };
|
|
930
|
+
return {
|
|
931
|
+
spin: true,
|
|
932
|
+
title: "Your agent is revising " + what.revising,
|
|
933
|
+
text: s.lastChange
|
|
934
|
+
? "Last change " + hhmm(s.lastChange) + " (" + ago(t - s.lastChange) + ") · this page updates by itself."
|
|
935
|
+
: "It has your comments since " + hhmm(state.revisingSince || t) + " · this page updates by itself.",
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
default:
|
|
939
|
+
return { icon: ICONS.check, title: workingNote() || "Saved", text: "This page updates by itself." };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function renderRevising() {
|
|
943
|
+
let node = document.getElementById("sm-revising");
|
|
944
|
+
const show = !!CFG.embedded && (isWorking() || state.phase === "revising" || state.phase === "handoff");
|
|
945
|
+
if (!show || state.phase === "approved") {
|
|
946
|
+
if (node && node.parentNode) node.parentNode.removeChild(node);
|
|
947
|
+
lastStripKey = null;
|
|
948
|
+
offsetSurface();
|
|
949
|
+
publishChrome();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const m = stripModel();
|
|
953
|
+
const key = JSON.stringify([m.tone, m.title, m.text, m.say, m.copy && !!state.agentPrompt]);
|
|
954
|
+
if (node && key === lastStripKey) return publishChrome();
|
|
955
|
+
lastStripKey = key;
|
|
956
|
+
if (!node) {
|
|
957
|
+
node = el("div", "sm-o sm-revising");
|
|
958
|
+
node.id = "sm-revising";
|
|
959
|
+
node.setAttribute("data-sm-overlay", "1");
|
|
960
|
+
stripHost().appendChild(node);
|
|
961
|
+
}
|
|
962
|
+
node.className = "sm-o sm-revising" + (m.tone ? " sm-revising--" + m.tone : "");
|
|
963
|
+
node.innerHTML = (m.spin ? '<div class="sm-spin sm-spin-lg"></div>' : '<span class="ic">' + (m.icon || ICONS.check) + "</span>") + '<div class="txt"><b></b><span></span></div>';
|
|
964
|
+
node.querySelector(".txt b").textContent = m.title;
|
|
965
|
+
node.querySelector(".txt span").textContent = m.text;
|
|
966
|
+
if (m.say) {
|
|
967
|
+
const say = el("span", "sm-say");
|
|
968
|
+
say.textContent = m.say;
|
|
969
|
+
node.querySelector(".txt span").append(" ", say);
|
|
970
|
+
}
|
|
971
|
+
if (m.copy) appendCopy(node.querySelector(".txt"));
|
|
972
|
+
offsetSurface();
|
|
973
|
+
publishChrome();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// A transient toast at the top ("Your agent picked it up at 14:32").
|
|
977
|
+
function showToast(title, detail) {
|
|
978
|
+
if (hosted) {
|
|
979
|
+
// the host shows it as a notice that goes away by itself
|
|
980
|
+
const id = "toast-" + ++toastSeq;
|
|
981
|
+
showNotice(id, { tone: "ok", title, text: detail || "", dismiss: true });
|
|
982
|
+
setTimeout(() => clearNotice(id), 9000);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const node = el("div", "sm-o sm-arrived");
|
|
986
|
+
node.setAttribute("data-sm-overlay", "1");
|
|
987
|
+
node.innerHTML = ICONS.check + '<div class="txt"><b></b><span></span></div>';
|
|
988
|
+
node.querySelector("b").textContent = title;
|
|
989
|
+
node.querySelector("span").textContent = detail || "";
|
|
990
|
+
document.body.appendChild(node);
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
node.classList.add("out");
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
995
|
+
}, 400);
|
|
996
|
+
}, 9000);
|
|
997
|
+
}
|
|
998
|
+
// The agent's next version landed: say so, loudly and briefly.
|
|
999
|
+
function showArrived(detail) {
|
|
1000
|
+
showToast("Your agent’s new version is ready", detail);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// After every answer: the user has ONE thing to do — paste the message into
|
|
1004
|
+
// their agent chat. The title says that; the line under it follows the copy
|
|
1005
|
+
// icon, which is the only thing that writes the clipboard (it is the user's).
|
|
1006
|
+
// Same words every time, whatever any agent process is doing.
|
|
1007
|
+
const HANDOFF_TITLE = "Paste this into your agent chat";
|
|
1008
|
+
function pasteKey() {
|
|
1009
|
+
return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || "") ? "⌘V" : "Ctrl+V";
|
|
1010
|
+
}
|
|
1011
|
+
function handoffLine(copied) {
|
|
1012
|
+
if (copied === true) return "Copied — press " + pasteKey() + " there and send.";
|
|
1013
|
+
if (copied === false) return "Select the message, copy it, paste it there and send.";
|
|
1014
|
+
return "Copy it, press " + pasteKey() + " there and send.";
|
|
1015
|
+
}
|
|
1016
|
+
// The copy icon: a click copies, the icon turns into a check for a moment.
|
|
1017
|
+
function copyIconButton(text, onCopied) {
|
|
1018
|
+
const b = el("button", "sm-copy-icon", ICONS.copy);
|
|
1019
|
+
b.type = "button";
|
|
1020
|
+
b.title = "Copy";
|
|
1021
|
+
b.setAttribute("aria-label", "Copy message");
|
|
1022
|
+
b.onclick = () =>
|
|
1023
|
+
copyText(text, (ok) => {
|
|
1024
|
+
if (onCopied) onCopied(ok);
|
|
1025
|
+
if (!ok) return;
|
|
1026
|
+
b.innerHTML = ICONS.check;
|
|
1027
|
+
b.classList.add("is-copied");
|
|
1028
|
+
b.title = "Copied";
|
|
1029
|
+
clearTimeout(b._reset);
|
|
1030
|
+
b._reset = setTimeout(() => {
|
|
1031
|
+
b.innerHTML = ICONS.copy;
|
|
1032
|
+
b.classList.remove("is-copied");
|
|
1033
|
+
b.title = "Copy";
|
|
1034
|
+
}, 2000);
|
|
1035
|
+
});
|
|
1036
|
+
return b;
|
|
1037
|
+
}
|
|
1038
|
+
function showHandoff() {
|
|
1039
|
+
state.handoffOpen = true;
|
|
1040
|
+
onSaveCtx();
|
|
1041
|
+
if (hosted) return publishChrome(); // the host's own dialog, from handoffModel()
|
|
1042
|
+
const old = document.getElementById("sm-handoff");
|
|
1043
|
+
if (old && old.parentNode) old.parentNode.removeChild(old);
|
|
1044
|
+
const veil = el("div", "sm-o sm-handoff");
|
|
1045
|
+
veil.id = "sm-handoff";
|
|
1046
|
+
veil.setAttribute("data-sm-overlay", "1");
|
|
1047
|
+
veil.setAttribute("role", "dialog");
|
|
1048
|
+
veil.setAttribute("aria-modal", "true");
|
|
1049
|
+
const box = el("div", "sm-handoff-box");
|
|
1050
|
+
box.innerHTML =
|
|
1051
|
+
'<div class="sm-handoff-head">' + ICONS.check + "<span></span></div>" +
|
|
1052
|
+
'<p class="sm-handoff-p big"></p>' +
|
|
1053
|
+
'<div class="sm-msgbox"><textarea class="sm-handoff-msg" readonly rows="2"></textarea></div>';
|
|
1054
|
+
box.querySelector(".sm-handoff-head span").textContent = HANDOFF_TITLE;
|
|
1055
|
+
const line = box.querySelector(".sm-handoff-p");
|
|
1056
|
+
line.textContent = handoffLine(null);
|
|
1057
|
+
const msg = box.querySelector(".sm-handoff-msg");
|
|
1058
|
+
msg.value = state.agentPrompt || "";
|
|
1059
|
+
let copy = null;
|
|
1060
|
+
if (state.agentPrompt) {
|
|
1061
|
+
copy = copyIconButton(state.agentPrompt, (ok) => {
|
|
1062
|
+
line.textContent = handoffLine(ok);
|
|
1063
|
+
if (!ok) msg.select();
|
|
1064
|
+
});
|
|
1065
|
+
box.querySelector(".sm-msgbox").appendChild(copy);
|
|
1066
|
+
}
|
|
1067
|
+
const row = el("div", "sm-handoff-row");
|
|
1068
|
+
const ok = el("button", "sm-handoff-btn primary", ICONS.check + "<span>Done</span>");
|
|
1069
|
+
ok.type = "button";
|
|
1070
|
+
ok.onclick = () => closeHandoff();
|
|
1071
|
+
row.appendChild(ok);
|
|
1072
|
+
box.appendChild(row);
|
|
1073
|
+
veil.appendChild(box);
|
|
1074
|
+
document.body.appendChild(veil);
|
|
1075
|
+
// the whole message shows, however long its path: the box grows to fit it
|
|
1076
|
+
msg.style.height = msg.scrollHeight + 2 + "px";
|
|
1077
|
+
// the next thing to do is the copy: the icon has the focus, so Enter copies
|
|
1078
|
+
(copy || ok).focus();
|
|
1079
|
+
}
|
|
1080
|
+
function closeHandoff() {
|
|
1081
|
+
state.handoffOpen = false;
|
|
1082
|
+
onSaveCtx();
|
|
1083
|
+
const old = document.getElementById("sm-handoff");
|
|
1084
|
+
if (old && old.parentNode) old.parentNode.removeChild(old);
|
|
1085
|
+
publishChrome();
|
|
1086
|
+
}
|
|
1087
|
+
function handoffModel() {
|
|
1088
|
+
return {
|
|
1089
|
+
title: HANDOFF_TITLE,
|
|
1090
|
+
text: handoffLine(null),
|
|
1091
|
+
message: state.agentPrompt || "",
|
|
1092
|
+
copy: state.agentPrompt || "",
|
|
1093
|
+
actions: [{ id: "handoff-close", label: "Done", primary: true }],
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
chromeActions["handoff-close"] = closeHandoff;
|
|
1097
|
+
// The badge brings the dialog back, with the message and its copy icon.
|
|
1098
|
+
chromeActions["handoff-open"] = () => {
|
|
1099
|
+
if (state.phase !== "handoff") return;
|
|
1100
|
+
showHandoff();
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// ---------- the chrome, published to the host ----------
|
|
1104
|
+
// Everything this page would paint above its content, as one object the
|
|
1105
|
+
// workspace page renders in its own chrome: the status line, the notices and
|
|
1106
|
+
// banners (in strip order), the revising strip, the hand-off dialog. Posted
|
|
1107
|
+
// only when it changed; a click on any of it comes back as a chrome event.
|
|
1108
|
+
function chromeModel() {
|
|
1109
|
+
const list = [];
|
|
1110
|
+
const act = (nid, actions) =>
|
|
1111
|
+
(actions || []).map((a, i) => {
|
|
1112
|
+
const id = nid + ":" + i;
|
|
1113
|
+
chromeActions[id] = a.onClick;
|
|
1114
|
+
return { id, label: a.label, primary: !!a.primary };
|
|
1115
|
+
});
|
|
1116
|
+
Object.keys(noticeModels).forEach((id) => {
|
|
1117
|
+
const n = noticeModels[id];
|
|
1118
|
+
list.push({ id, tone: n.tone || "info", title: n.title || "", text: n.text || "", spin: !!n.spin, dismiss: !!n.dismiss, copy: n.copy ? state.agentPrompt || "" : "", actions: act(id, n.actions) });
|
|
1119
|
+
});
|
|
1120
|
+
// a state is the status badge's, never a banner: only "asked again" (the user must act) stays
|
|
1121
|
+
banners.forEach((b) => b.id !== "approved" && b.id !== "not-asked" && list.push(b));
|
|
1122
|
+
const strip = !!CFG.embedded && (isWorking() || state.phase === "revising" || state.phase === "handoff") && state.phase !== "approved";
|
|
1123
|
+
if (strip) {
|
|
1124
|
+
// revising / working / answer saved are the badge's; the strip is a notice
|
|
1125
|
+
// only when something went wrong (the agent seems to have stopped). The
|
|
1126
|
+
// paste itself was asked once, by the dialog after the click.
|
|
1127
|
+
const m = stripModel();
|
|
1128
|
+
if (m.tone === "warn") list.push({ id: "revising", tone: "warn", title: m.title, text: m.text, say: m.say || "", spin: false, copy: m.copy ? state.agentPrompt || "" : "" });
|
|
1129
|
+
}
|
|
1130
|
+
const w = waiterModel;
|
|
1131
|
+
return {
|
|
1132
|
+
status: w ? { text: w.text, detail: w.detail || "", spin: !!w.spin, tone: w.cls === "on" ? "ok" : w.cls === "warn" ? "warn" : w.cls === "err" ? "bad" : "", action: w.action || "" } : null,
|
|
1133
|
+
notices: list,
|
|
1134
|
+
dialog: state.handoffOpen ? handoffModel() : null,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function publishChrome() {
|
|
1138
|
+
if (!hosted) return;
|
|
1139
|
+
const chrome = chromeModel();
|
|
1140
|
+
const key = JSON.stringify(chrome);
|
|
1141
|
+
if (key === lastChromeKey) return;
|
|
1142
|
+
lastChromeKey = key;
|
|
1143
|
+
try {
|
|
1144
|
+
window.parent.postMessage({ type: "slidemind-view-chrome", chrome }, location.origin);
|
|
1145
|
+
} catch {
|
|
1146
|
+
/* a parent on another origin cannot host this page */
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (hosted) {
|
|
1150
|
+
window.addEventListener("message", (e) => {
|
|
1151
|
+
const d = e.data;
|
|
1152
|
+
if (e.source !== window.parent || e.origin !== location.origin || !d || d.type !== "slidemind-view-chrome-event" || !d.event) return;
|
|
1153
|
+
const ev = d.event;
|
|
1154
|
+
if (ev.kind === "dismiss") clearNotice(String(ev.id));
|
|
1155
|
+
else if (ev.kind === "action" && typeof chromeActions[ev.id] === "function") chromeActions[ev.id]();
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// ---------- the decision ----------
|
|
1160
|
+
// One attempt at a time (the in-flight latch: a double click cannot send
|
|
1161
|
+
// twice). The attempt — with its operationId — is persisted, so a reload in
|
|
1162
|
+
// the middle of a retry re-sends the SAME request.
|
|
1163
|
+
function decide(spec) {
|
|
1164
|
+
if (!SES || state.pending) return false;
|
|
1165
|
+
const body = { decision: spec.decision, comments: spec.comments || [] };
|
|
1166
|
+
if (typeof spec.editedPlan === "string") body.editedPlan = spec.editedPlan;
|
|
1167
|
+
if (typeof spec.design === "string") body.design = spec.design;
|
|
1168
|
+
body.operationId = SES.newOperationId();
|
|
1169
|
+
if (CFG.subjectHash) body.expectedHash = CFG.subjectHash;
|
|
1170
|
+
state.pending = { operationId: body.operationId, decision: spec.decision, body, startedAt: now() };
|
|
1171
|
+
authFromDecision = false;
|
|
1172
|
+
clearNotice("auth");
|
|
1173
|
+
clearNotice("failed");
|
|
1174
|
+
onSaveCtx();
|
|
1175
|
+
run();
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
function run() {
|
|
1179
|
+
const p = state.pending;
|
|
1180
|
+
SES.decide(p, {
|
|
1181
|
+
onSuccess(ack) {
|
|
1182
|
+
state.pending = null;
|
|
1183
|
+
clearNotice("busy");
|
|
1184
|
+
clearNotice("offline");
|
|
1185
|
+
clearNotice("failed");
|
|
1186
|
+
succeeded(p, ack);
|
|
1187
|
+
},
|
|
1188
|
+
onBusy(info) {
|
|
1189
|
+
showNotice("busy", {
|
|
1190
|
+
tone: "warn",
|
|
1191
|
+
spin: true,
|
|
1192
|
+
title: "Your agent is working on this deck right now",
|
|
1193
|
+
text: "Your answer is kept and sent automatically as soon as it is free.",
|
|
1194
|
+
});
|
|
1195
|
+
},
|
|
1196
|
+
onOffline() {
|
|
1197
|
+
clearNotice("busy");
|
|
1198
|
+
showNotice("offline", { tone: "warn", spin: true, title: "Connection lost", text: "Your answer is kept and sent as soon as the connection is back." });
|
|
1199
|
+
},
|
|
1200
|
+
onFailed(o) {
|
|
1201
|
+
state.pending = null;
|
|
1202
|
+
clearNotice("busy");
|
|
1203
|
+
clearNotice("offline");
|
|
1204
|
+
onSaveCtx();
|
|
1205
|
+
failed(p, o);
|
|
1206
|
+
onChange();
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
onChange();
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function succeeded(p, ack) {
|
|
1213
|
+
readAck(ack);
|
|
1214
|
+
state.decision = p.decision;
|
|
1215
|
+
state.task = ack.task || null;
|
|
1216
|
+
state.decidedAt = now();
|
|
1217
|
+
state.answered = null;
|
|
1218
|
+
// Workspace: always the same hand-over — the one dialog with the message to
|
|
1219
|
+
// paste. Nothing is copied on the user's behalf: their copy-icon click does it.
|
|
1220
|
+
if (CFG.embedded) state.phase = "handoff";
|
|
1221
|
+
onSaveCtx();
|
|
1222
|
+
call("onDecided", p.decision, ack, p.body);
|
|
1223
|
+
const after = () => {
|
|
1224
|
+
renderRevising();
|
|
1225
|
+
renderWaiter();
|
|
1226
|
+
onChange();
|
|
1227
|
+
};
|
|
1228
|
+
if (state.phase === "handoff") showHandoff();
|
|
1229
|
+
after();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function failed(p, o) {
|
|
1233
|
+
const approve = p.decision === "approve";
|
|
1234
|
+
switch (o.code) {
|
|
1235
|
+
case "stale_subject":
|
|
1236
|
+
case "local_conflict": {
|
|
1237
|
+
// The preview is older than the slides: keep the page (a reload would
|
|
1238
|
+
// show the same preview again), mute Approve, say why.
|
|
1239
|
+
if (o.details && o.details.previewStale) {
|
|
1240
|
+
setPreviewStale(true);
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
const note = {
|
|
1244
|
+
tone: "info",
|
|
1245
|
+
title: "Your agent updated " + what.revising + " — here is the new version.",
|
|
1246
|
+
text: approve ? "Have a look, then approve again." : "Your comments are kept.",
|
|
1247
|
+
};
|
|
1248
|
+
if (o.details && o.details.current && o.details.current === CFG.subjectHash) {
|
|
1249
|
+
// this page already shows the current version (an answer re-sent after
|
|
1250
|
+
// a reload): reloading would show the same page again
|
|
1251
|
+
showNotice("note", Object.assign({ dismiss: true }, note));
|
|
1252
|
+
} else if (reloadBlocker() === "edits") {
|
|
1253
|
+
showNotice("newer", newerNotice("edits"));
|
|
1254
|
+
} else {
|
|
1255
|
+
SES.noteForNextLoad(note);
|
|
1256
|
+
reload();
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case "already_answered":
|
|
1261
|
+
answeredElsewhere(o.details || {});
|
|
1262
|
+
break;
|
|
1263
|
+
case "unauthorized":
|
|
1264
|
+
// the heartbeat may still pass (it rides the page's cookie, the answer
|
|
1265
|
+
// its own token): this notice stays until the reviewer tries again
|
|
1266
|
+
authFromDecision = true;
|
|
1267
|
+
showNotice("auth", Object.assign(authNotice(), { dismiss: true }));
|
|
1268
|
+
break;
|
|
1269
|
+
case "offline":
|
|
1270
|
+
// standalone only — the workspace page waits for the workspace instead
|
|
1271
|
+
showNotice("failed", {
|
|
1272
|
+
tone: "error",
|
|
1273
|
+
title: approve ? "Your approval did not arrive" : "Your comments did not arrive",
|
|
1274
|
+
text: "This page lost its connection to your agent. What you wrote is kept — go back to your agent and ask it to open the review again; this page picks it up by itself.",
|
|
1275
|
+
});
|
|
1276
|
+
call("onLost");
|
|
1277
|
+
break;
|
|
1278
|
+
case "validation_failed":
|
|
1279
|
+
showNotice("failed", { tone: "error", title: "This could not be sent", text: plainMessage(o.message) || "Something in it was not accepted. What you wrote is kept.", dismiss: true });
|
|
1280
|
+
break;
|
|
1281
|
+
default:
|
|
1282
|
+
showNotice("failed", {
|
|
1283
|
+
tone: "error",
|
|
1284
|
+
title: "This could not be sent",
|
|
1285
|
+
text: (plainMessage(o.message) ? plainMessage(o.message) + " " : "") + "What you wrote is kept — try again.",
|
|
1286
|
+
dismiss: true,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
call("onDecisionFailed", o);
|
|
1290
|
+
}
|
|
1291
|
+
// A server message, minus anything that reads like a command to type.
|
|
1292
|
+
function plainMessage(m) {
|
|
1293
|
+
const s = String(m || "").trim();
|
|
1294
|
+
if (!s || /`|slidemind [a-z]/i.test(s)) return "";
|
|
1295
|
+
return /[.!?]$/.test(s) ? s : s + ".";
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Another window answered already: switch to that answer, no hand-off.
|
|
1299
|
+
function answeredElsewhere(details) {
|
|
1300
|
+
const d = details || {};
|
|
1301
|
+
const approvedThere = d.action === "approve";
|
|
1302
|
+
state.answered = { action: d.action || null, at: d.at || null };
|
|
1303
|
+
if (CFG.embedded) {
|
|
1304
|
+
if (approvedThere) state.phase = "answered";
|
|
1305
|
+
else {
|
|
1306
|
+
state.decision = "comments";
|
|
1307
|
+
state.phase = "revising";
|
|
1308
|
+
state.revisingSince = (d.at && Date.parse(d.at)) || now();
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
onSaveCtx();
|
|
1312
|
+
const at = d.at ? " at " + hhmm(d.at) : "";
|
|
1313
|
+
// Embedded, an approval elsewhere IS the strip ("Already answered —
|
|
1314
|
+
// approved at …"); everything else gets the one-line notice.
|
|
1315
|
+
if (!(CFG.embedded && approvedThere)) {
|
|
1316
|
+
showNotice("answered", {
|
|
1317
|
+
tone: "info",
|
|
1318
|
+
title: d.action ? "Already answered — " + (approvedThere ? "approved" : "comments sent") + at : "Already answered in another window",
|
|
1319
|
+
text: approvedThere ? "This was answered in another window. Nothing else to do here." : "This was answered in another window. What you wrote here is kept.",
|
|
1320
|
+
dismiss: true,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
call("onAlreadyAnswered", state.answered);
|
|
1324
|
+
renderRevising();
|
|
1325
|
+
renderWaiter();
|
|
1326
|
+
onChange();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ---------- the hand-over after the answer ----------
|
|
1330
|
+
function pickedUp(atIso) {
|
|
1331
|
+
const at = Date.parse(atIso) || now();
|
|
1332
|
+
state.pickedUpAt = at;
|
|
1333
|
+
if (state.handoffOpen) closeHandoff();
|
|
1334
|
+
if (state.decision === "approve") {
|
|
1335
|
+
state.phase = "approved";
|
|
1336
|
+
onSaveCtx();
|
|
1337
|
+
call("onApprovalPickedUp", at);
|
|
1338
|
+
} else {
|
|
1339
|
+
state.phase = "revising";
|
|
1340
|
+
state.revisingSince = at;
|
|
1341
|
+
onSaveCtx();
|
|
1342
|
+
showToast("Your agent picked it up at " + hhmm(at), "It is revising " + what.revising + " now.");
|
|
1343
|
+
}
|
|
1344
|
+
renderRevising();
|
|
1345
|
+
renderWaiter();
|
|
1346
|
+
onChange();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// The done veil: brand mark + wordmark + a status line. `hide` are the
|
|
1350
|
+
// overlay's own nodes to take off screen behind it.
|
|
1351
|
+
function showVeil(statusHtml, subText, subPulse, hide) {
|
|
1352
|
+
if (bar) bar.style.display = "none";
|
|
1353
|
+
const host = document.getElementById("sm-strips");
|
|
1354
|
+
if (host) host.style.display = "none";
|
|
1355
|
+
(hide || []).forEach((n) => {
|
|
1356
|
+
if (n) n.style.display = "none";
|
|
1357
|
+
});
|
|
1358
|
+
const existing = document.querySelector(".sm-veil[data-sm-overlay]");
|
|
1359
|
+
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
|
1360
|
+
const veil = el("div", "sm-o sm-veil");
|
|
1361
|
+
veil.setAttribute("data-sm-overlay", "1");
|
|
1362
|
+
veil.appendChild(logoNode("sm-logo", "-v"));
|
|
1363
|
+
veil.appendChild(el("div", "word", wordmarkHtml()));
|
|
1364
|
+
veil.appendChild(el("div", "status", statusHtml));
|
|
1365
|
+
const sub = el("div", "sub" + (subPulse ? " sm-pulse" : ""));
|
|
1366
|
+
sub.textContent = subText;
|
|
1367
|
+
veil.appendChild(sub);
|
|
1368
|
+
document.body.appendChild(veil);
|
|
1369
|
+
return veil;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function requestTabClose() {
|
|
1373
|
+
setTimeout(() => {
|
|
1374
|
+
try {
|
|
1375
|
+
window.open("", "_self");
|
|
1376
|
+
window.close();
|
|
1377
|
+
} catch {
|
|
1378
|
+
// Some browsers only allow script-opened tabs to close themselves.
|
|
1379
|
+
}
|
|
1380
|
+
}, 120);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ---------- the round context ----------
|
|
1384
|
+
function ctxFields() {
|
|
1385
|
+
return {
|
|
1386
|
+
phase: state.phase,
|
|
1387
|
+
decision: state.decision,
|
|
1388
|
+
task: state.task,
|
|
1389
|
+
decidedAt: state.decidedAt,
|
|
1390
|
+
pickedUpAt: state.pickedUpAt,
|
|
1391
|
+
revisingSince: state.revisingSince,
|
|
1392
|
+
handoffOpen: state.handoffOpen,
|
|
1393
|
+
answered: state.answered,
|
|
1394
|
+
prompt: state.agentPrompt,
|
|
1395
|
+
pending: state.pending,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
// A changed nonce that is NOT the next round (see gate-session restore's
|
|
1399
|
+
// `hold`): nothing new was asked, the content moved because of the answer this
|
|
1400
|
+
// page gave — an approve that committed inline edits, an approve the agent
|
|
1401
|
+
// never asked for (the server records the request on the user's behalf), the
|
|
1402
|
+
// agent's own edits while it revises. The hand-over must survive that reload:
|
|
1403
|
+
// an answer still on its way, waiting to be pasted, approved or revised.
|
|
1404
|
+
function holdsRound(ctx) {
|
|
1405
|
+
if (!CFG.embedded || CFG.asked || !ctx) return false;
|
|
1406
|
+
if (ctx.pending && ctx.pending.operationId) return true;
|
|
1407
|
+
return !!ctx.working && (ctx.phase === "handoff" || ctx.phase === "revising" || ctx.phase === "approved" || ctx.phase === "answered");
|
|
1408
|
+
}
|
|
1409
|
+
// Restore the hand-over a previous load of this round saved. Returns whether
|
|
1410
|
+
// this load is the version the user was waiting for (the agent's next round).
|
|
1411
|
+
function restoreCtx(ctx, working, newRound) {
|
|
1412
|
+
if (!ctx) return false;
|
|
1413
|
+
if (typeof ctx.prompt === "string") state.agentPrompt = ctx.prompt;
|
|
1414
|
+
if (working) {
|
|
1415
|
+
state.phase = ctx.phase || null;
|
|
1416
|
+
state.decision = ctx.decision || null;
|
|
1417
|
+
state.task = ctx.task || null;
|
|
1418
|
+
state.decidedAt = ctx.decidedAt || null;
|
|
1419
|
+
// a page from before the paste protocol saved "handing": it is the hand-off now
|
|
1420
|
+
if (state.phase === "handing") state.phase = "handoff";
|
|
1421
|
+
state.pickedUpAt = ctx.pickedUpAt || null;
|
|
1422
|
+
state.revisingSince = typeof ctx.revisingSince === "number" ? ctx.revisingSince : null;
|
|
1423
|
+
state.handoffOpen = !!ctx.handoffOpen;
|
|
1424
|
+
state.answered = ctx.answered || null;
|
|
1425
|
+
}
|
|
1426
|
+
// A decision still on its way survives the reload; resume() re-sends it.
|
|
1427
|
+
if (CFG.embedded && ctx.pending && ctx.pending.body && ctx.pending.operationId) restoredPending = ctx.pending;
|
|
1428
|
+
return !!(newRound && ctx.decision === "comments" && (ctx.phase === "revising" || ctx.phase === "handing" || ctx.phase === "handoff"));
|
|
1429
|
+
}
|
|
1430
|
+
// Called by the overlay once its page is built: the next-load note, the
|
|
1431
|
+
// hand-over in progress, and an answer that was still on its way.
|
|
1432
|
+
function resume() {
|
|
1433
|
+
const note = SES ? SES.consumeNote() : null;
|
|
1434
|
+
if (note) showNotice("note", { tone: note.tone || "info", title: note.title, text: note.text, dismiss: true });
|
|
1435
|
+
if (state.phase === "handoff" && state.handoffOpen) showHandoff();
|
|
1436
|
+
renderStale();
|
|
1437
|
+
if (restoredPending) {
|
|
1438
|
+
// Re-sent as it was, same operationId, whatever this page now shows: the
|
|
1439
|
+
// reload may have come from the answer itself (the workspace reloads the
|
|
1440
|
+
// frame when the plan text moves), and the server replays an answer it
|
|
1441
|
+
// already saved; one it never saw against older content comes back
|
|
1442
|
+
// stale_subject, which says so.
|
|
1443
|
+
state.pending = restoredPending;
|
|
1444
|
+
restoredPending = null;
|
|
1445
|
+
run();
|
|
1446
|
+
}
|
|
1447
|
+
renderRevising();
|
|
1448
|
+
renderWaiter();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// A decision's ack: the message the user pastes into their chat with the agent.
|
|
1452
|
+
function readAck(ack) {
|
|
1453
|
+
if (ack && ack.message) state.agentPrompt = ack.message;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
state,
|
|
1458
|
+
ICONS,
|
|
1459
|
+
el,
|
|
1460
|
+
logoNode,
|
|
1461
|
+
wordmarkHtml,
|
|
1462
|
+
mkButton,
|
|
1463
|
+
injectAssets,
|
|
1464
|
+
buildBar,
|
|
1465
|
+
fitBar,
|
|
1466
|
+
notAskedYet,
|
|
1467
|
+
alreadyApproved,
|
|
1468
|
+
reviewNotNeeded,
|
|
1469
|
+
askedAgain,
|
|
1470
|
+
decisionPending,
|
|
1471
|
+
isPreviewStale,
|
|
1472
|
+
inFlight,
|
|
1473
|
+
offsetSurface,
|
|
1474
|
+
showNotice,
|
|
1475
|
+
clearNotice,
|
|
1476
|
+
showNotAskedBanner,
|
|
1477
|
+
showApprovedBanner,
|
|
1478
|
+
copyMessageButton,
|
|
1479
|
+
renderRevising,
|
|
1480
|
+
showToast,
|
|
1481
|
+
showArrived,
|
|
1482
|
+
showAskedAgainUnchanged,
|
|
1483
|
+
showHandoff,
|
|
1484
|
+
showVeil,
|
|
1485
|
+
requestTabClose,
|
|
1486
|
+
decide,
|
|
1487
|
+
restoreCtx,
|
|
1488
|
+
holdsRound,
|
|
1489
|
+
resume,
|
|
1490
|
+
ctxFields,
|
|
1491
|
+
readAck,
|
|
1492
|
+
hhmm,
|
|
1493
|
+
};
|
|
1494
|
+
}
|