agent-canvas 0.1.40 → 0.1.42

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.
Files changed (38) hide show
  1. package/README.md +18 -1
  2. package/bin/agent-canvas.ts +5 -1
  3. package/bin/lib/commands/exec.test.ts +27 -0
  4. package/bin/lib/commands/exec.ts +129 -0
  5. package/bin/lib/commands/watch.ts +8 -1
  6. package/bin/lib/config.ts +1 -0
  7. package/bin/lib/daemon-lifecycle.ts +3 -1
  8. package/bin/lib/helpers.test.ts +48 -0
  9. package/bin/lib/helpers.ts +65 -15
  10. package/bin/lib/instructions/component-secretinput.md +61 -0
  11. package/bin/lib/instructions/flow-feature.md +1 -1
  12. package/bin/lib/instructions/flow-plan.md +1 -1
  13. package/bin/lib/instructions/flow-runbook.md +77 -0
  14. package/bin/lib/instructions/overview.md +32 -12
  15. package/daemon/client/AnnotationProvider.tsx +2 -7
  16. package/daemon/client/App.tsx +12 -2
  17. package/daemon/client/annotationDraft.test.ts +16 -1
  18. package/daemon/client/annotationDraft.ts +15 -0
  19. package/daemon/client/clientApi.ts +41 -0
  20. package/daemon/client/components/SecretInput.tsx +156 -0
  21. package/daemon/client/components/index.ts +1 -0
  22. package/daemon/dist/client.css +1 -1
  23. package/daemon/dist/client.js +28 -28
  24. package/daemon/dist/components.js +42 -40
  25. package/daemon/src/compiler.test.ts +5 -0
  26. package/daemon/src/compiler.ts +2 -2
  27. package/daemon/src/handlers/api.ts +81 -0
  28. package/daemon/src/handlers/utils.ts +1 -1
  29. package/daemon/src/paths.ts +3 -0
  30. package/daemon/src/secrets-api.test.ts +123 -0
  31. package/daemon/src/server.ts +9 -5
  32. package/daemon/src/session-secrets.test.ts +83 -0
  33. package/daemon/src/session.ts +55 -11
  34. package/package.json +1 -1
  35. package/skills/canvas/SKILL.md +18 -7
  36. package/workers/canvas-share/public/assets/client.css +1 -1
  37. package/workers/canvas-share/public/assets/client.js +28 -28
  38. package/workers/canvas-share/public/assets/components.js +42 -40
@@ -4,16 +4,17 @@ Canvas opens a rich, annotatable document in the user's browser. You write JSX,
4
4
 
5
5
  Use it whenever structured visual communication beats inline chat: plans, reviews, decisions, explanations, discovery interviews.
6
6
 
7
- **Note**: Replace all `<session-id>` references below with your actual canvas session ID (provided in the skill header).
7
+ **Note**: Replace all `<session-id>` references below with the stable canvas
8
+ session ID resolved by the skill.
8
9
 
9
10
  ## Core Workflow
10
11
 
11
12
  ### 1. Write JSX
12
13
 
13
- Use the **Write** tool to create `.jsx` files in `$TMPDIR/agent-canvas/<session-id>/`:
14
+ Use the **Write** tool to create `.jsx` files in `${TMPDIR:-/tmp}/agent-canvas/<session-id>/`:
14
15
 
15
16
  ```jsx
16
- // $TMPDIR/agent-canvas/<session-id>/plan.jsx
17
+ // ${TMPDIR:-/tmp}/agent-canvas/<session-id>/plan.jsx
17
18
 
18
19
  <Section title="Authentication Redesign">
19
20
  A proposal to replace session-based auth with JWT tokens.
@@ -41,7 +42,7 @@ Components are auto-available — no imports needed. The file can be a JSX fragm
41
42
  Push the directory to open all canvases in the browser:
42
43
 
43
44
  ```bash
44
- bunx agent-canvas push $TMPDIR/agent-canvas/<session-id>/ --session <session-id> --label "Implementation Plan"
45
+ bunx agent-canvas push ${TMPDIR:-/tmp}/agent-canvas/<session-id>/ --session <session-id> --label "Implementation Plan"
45
46
  ```
46
47
 
47
48
  All `*.jsx` files in the directory are pushed as a snapshot. Each file appears as a separate tab. **Always show the `browserUrl` from the output to the user.**
@@ -52,16 +53,32 @@ Options:
52
53
 
53
54
  ### 3. Watch for feedback
54
55
 
55
- Run the watch command **in the background** using the Bash tool's `run_in_background` parameter:
56
+ Start watching immediately after every push:
56
57
 
57
58
  ```bash
58
- # Use Bash tool with run_in_background: true
59
59
  bunx agent-canvas watch --session <session-id>
60
60
  ```
61
61
 
62
- You will be **automatically notified** when the user submits feedback. Do NOT poll, sleep, or proactively check. Just stop and wait.
62
+ How to run the watcher depends on the host:
63
63
 
64
- **Important**: The push → watch sequence is atomic. Never push without watching. After starting the background watch, do not continue with other work unless instructed.
64
+ - **Codex**: Keep the watcher attached to the current agent turn. Run it without
65
+ detaching or backgrounding it. If the shell tool yields a process/session
66
+ handle, call `write_stdin` on that same handle until the command returns the
67
+ feedback. Do not end the agent turn while the watcher is still running.
68
+ - **Claude Code**: Run the watcher in the background using the Bash tool's
69
+ `run_in_background` parameter. You will be **automatically notified** when the
70
+ user submits feedback. Do NOT poll, sleep, or proactively check. Just stop and
71
+ wait.
72
+ - **Other hosts**: Keep the watcher in the foreground unless the host explicitly
73
+ guarantees that background-process completion wakes the agent.
74
+
75
+ In Codex, a wait may yield before feedback arrives. Continue waiting on the same
76
+ process handle; do not start another watcher. Keep user-facing progress updates
77
+ brief and no more than 60 seconds apart while waiting.
78
+
79
+ **Important**: The push → watch sequence is atomic. Never push without starting
80
+ the host-specific watcher immediately afterward. Do not continue with unrelated
81
+ work while waiting for feedback.
65
82
 
66
83
  ### 4. Iterate
67
84
 
@@ -82,7 +99,7 @@ Returns immediately — prints feedback if available, otherwise no output.
82
99
  Use different files for different phases or concerns:
83
100
 
84
101
  ```
85
- $TMPDIR/agent-canvas/<session-id>/
102
+ ${TMPDIR:-/tmp}/agent-canvas/<session-id>/
86
103
  discovery.jsx # Discovery interview
87
104
  requirements.jsx # Requirements spec
88
105
  plan.jsx # Implementation plan
@@ -92,7 +109,7 @@ Write new files as phases progress, then push the directory. Previous files rema
92
109
 
93
110
  ## File Location
94
111
 
95
- All canvas files go in `$TMPDIR/agent-canvas/<session-id>/`. This is a system temp directory — no `.gitignore` needed.
112
+ All canvas files go in `${TMPDIR:-/tmp}/agent-canvas/<session-id>/`. This is a system temp directory — no `.gitignore` needed.
96
113
 
97
114
  ## Components
98
115
 
@@ -121,6 +138,7 @@ All components are globally available in canvas JSX — no imports needed. For i
121
138
  - **Choice** — single-select radio group. Props: `id`, `label`, `options`, `required`
122
139
  - **MultiChoice** — multi-select checkboxes. Props: `id`, `label`, `options`, `required`
123
140
  - **UserInput** — free text input. Props: `id`, `label`, `placeholder`, `required`
141
+ - **SecretInput** — local in-memory secret handoff. Props: `id`, `label`, `env`, `placeholder`, `required`
124
142
  - **RangeInput** — numeric slider. Props: `id`, `label`, `min`, `max`, `minLabel`, `maxLabel`, `required`
125
143
 
126
144
  ### Advanced
@@ -146,6 +164,7 @@ User wants something built/changed?
146
164
  User wants to understand something? → EXPLAIN flow
147
165
  User wants review/audit? → REVIEW flow
148
166
  User wants to make a decision? → DECISION flow
167
+ Agent needs the user to complete manual steps? → RUNBOOK flow
149
168
  ```
150
169
 
151
170
  **Adapt dynamically**: flows are guidelines, not rigid pipelines. Skip or combine phases when appropriate:
@@ -167,17 +186,18 @@ User wants to make a decision? → DECISION flow
167
186
  2. **Announce** briefly: "I'll start with discovery, then create a detailed plan."
168
187
  3. **Write canvas JSX** with the Write tool
169
188
  4. **Push + show browserUrl** to the user
170
- 5. **IMMEDIATELY watch in background** — the push → watch sequence is atomic
189
+ 5. **IMMEDIATELY watch using the host-specific strategy above** — the push → watch sequence is atomic
171
190
  6. **Read feedback** — check for annotations, answers, context files
172
191
  7. **Edit and re-push + watch**, or advance to next phase
173
192
  8. **After implementation**, push a summary canvas and **watch for feedback** — the user may want to respond
174
193
 
175
194
  ## Important Rules
176
195
 
177
- - **Never `cd`** before running `bunx agent-canvas` commands. The daemon uses the current working directory to resolve project file paths (`<FilePreview>`, file browser). Always run commands from the project root. Since canvas files are in `$TMPDIR`, use absolute paths to reference them — e.g. `bunx agent-canvas push $TMPDIR/agent-canvas/<session-id>/`.
196
+ - **Never `cd`** before running `bunx agent-canvas` commands. The daemon uses the current working directory to resolve project file paths (`<FilePreview>`, file browser). Always run commands from the project root. Since canvas files are in `${TMPDIR:-/tmp}`, use absolute paths to reference them — e.g. `bunx agent-canvas push ${TMPDIR:-/tmp}/agent-canvas/<session-id>/`.
178
197
  - **Write** canvas files using the Write tool. Never use bash heredocs.
179
198
  - **Edit** canvas files using the Edit tool. Never rewrite entire files.
180
199
  - Every `<Item>` and interactive component needs a unique `id`.
200
+ - Use `<SecretInput>`, never `<UserInput>`, for credentials. Read `component-secretinput` and `flow-runbook` first.
181
201
  - `<FilePreview path="...">` paths are relative to project root.
182
202
  - Read any files the user added to context (listed under "Added context" in feedback).
183
203
  - Always push a summary canvas after implementation, even if brief.
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useCallback, useEffect, useRef, useMemo } from "react";
2
2
  import { AnnotationCtx } from "#canvas/runtime";
3
3
  import type { Annotation, AnnotationContext, PlanResponse, FeedbackEntry, AnnotationContextValue } from "#canvas/runtime";
4
+ import { clearPersistedDraft } from "./annotationDraft";
4
5
  import { generateAnnotationId } from "./utils";
5
6
 
6
7
  // Re-export types for convenience
@@ -44,12 +45,6 @@ function savePersisted(sessionId: string, revision: number, state: PersistedStat
44
45
  } catch {}
45
46
  }
46
47
 
47
- function clearPersisted(sessionId: string, revision: number) {
48
- try {
49
- localStorage.removeItem(storageKey(sessionId, revision));
50
- } catch {}
51
- }
52
-
53
48
  export function AnnotationProvider({ sessionId, revision, isReadOnly, remoteAnnotations, children }: AnnotationProviderProps) {
54
49
  const [localAnnotations, setAnnotations] = useState<Annotation[]>(() => {
55
50
  const saved = loadPersisted(sessionId, revision);
@@ -164,7 +159,7 @@ export function AnnotationProvider({ sessionId, revision, isReadOnly, remoteAnno
164
159
  setActiveAnnotationId(null);
165
160
  setResponses(new Map());
166
161
  setFeedbackEntries(new Map());
167
- clearPersisted(sessionId, revision);
162
+ clearPersistedDraft(localStorage, sessionId, revision);
168
163
  }, [sessionId, revision]);
169
164
 
170
165
  return (
@@ -21,7 +21,7 @@ import { ShareDialog, ShareButton, type ShareEntry } from "./ShareDialog";
21
21
  import { ReviewerIdentityDialog } from "./ReviewerIdentityDialog";
22
22
  import type { Annotation } from "#canvas/runtime";
23
23
  import { MODE, fetchMeta, FS_AVAILABLE, WS_AVAILABLE, submitSharedFeedback, getReviewerIdentity, setReviewerIdentity } from "./clientApi";
24
- import { carryUnsubmittedDraft } from "./annotationDraft";
24
+ import { carryUnsubmittedDraft, clearPersistedDraft } from "./annotationDraft";
25
25
 
26
26
  export type ActiveView = { type: "overview" } | { type: "canvas"; filename: string } | { type: "file"; path: string };
27
27
 
@@ -314,6 +314,9 @@ function App() {
314
314
  .then((data: any) => {
315
315
  if (data.currentRevision) {
316
316
  const nextRevisions: RevisionInfo[] = data.revisions || [];
317
+ for (const revision of nextRevisions) {
318
+ if (revision.hasFeedback) clearPersistedDraft(localStorage, sessionId, revision.revision);
319
+ }
317
320
  carryUnsubmittedDraft(localStorage, sessionId, data.currentRevision, nextRevisions);
318
321
  setCurrentRevision(data.currentRevision);
319
322
  setSelectedRevision(data.currentRevision);
@@ -407,7 +410,13 @@ function App() {
407
410
  }
408
411
  }
409
412
  if (data.type === "revision-updated") {
410
- if (data.revisions) setRevisions(data.revisions);
413
+ if (Array.isArray(data.revisions)) {
414
+ const nextRevisions: RevisionInfo[] = data.revisions;
415
+ for (const revision of nextRevisions) {
416
+ if (revision.hasFeedback) clearPersistedDraft(localStorage, sessionId, revision.revision);
417
+ }
418
+ setRevisions(nextRevisions);
419
+ }
411
420
  }
412
421
  if (data.type === "watcher-status") {
413
422
  setAgentWatching(!!data.watching);
@@ -448,6 +457,7 @@ function App() {
448
457
  annotations: structuredAnnotations,
449
458
  generalNote: feedback,
450
459
  });
460
+ clearPersistedDraft(localStorage, sessionId, selectedRevision);
451
461
  setSharedFeedbackSubmitted(true);
452
462
  setToast({ kind: "success", message: "Feedback submitted. The canvas author will see it shortly." });
453
463
  } catch (e: any) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { carryUnsubmittedDraft } from "./annotationDraft";
2
+ import { carryUnsubmittedDraft, clearPersistedDraft } from "./annotationDraft";
3
3
 
4
4
  class MemoryStorage {
5
5
  private readonly values = new Map<string, string>();
@@ -11,12 +11,27 @@ class MemoryStorage {
11
11
  setItem(key: string, value: string): void {
12
12
  this.values.set(key, value);
13
13
  }
14
+
15
+ removeItem(key: string): void {
16
+ this.values.delete(key);
17
+ }
14
18
  }
15
19
 
16
20
  const sessionId = "session";
17
21
  const key = (revision: number) => `canvas:${sessionId}:rev:${revision}`;
18
22
 
19
23
  describe("carryUnsubmittedDraft", () => {
24
+ test("clears a submitted draft and its carry marker", () => {
25
+ const storage = new MemoryStorage();
26
+ storage.setItem(key(2), JSON.stringify({ generalNote: "Submitted" }));
27
+ storage.setItem(`${key(2)}:draft-handled`, "1");
28
+
29
+ clearPersistedDraft(storage, sessionId, 2);
30
+
31
+ expect(storage.getItem(key(2))).toBeNull();
32
+ expect(storage.getItem(`${key(2)}:draft-handled`)).toBeNull();
33
+ });
34
+
20
35
  test("carries the latest unsent draft into a new revision", () => {
21
36
  const storage = new MemoryStorage();
22
37
  storage.setItem(key(2), JSON.stringify({
@@ -8,6 +8,10 @@ interface AnnotationStorage {
8
8
  setItem(key: string, value: string): void;
9
9
  }
10
10
 
11
+ interface RemovableAnnotationStorage {
12
+ removeItem(key: string): void;
13
+ }
14
+
11
15
  function storageKey(sessionId: string, revision: number): string {
12
16
  return `canvas:${sessionId}:rev:${revision}`;
13
17
  }
@@ -16,6 +20,17 @@ function handledKey(sessionId: string, revision: number): string {
16
20
  return `${storageKey(sessionId, revision)}:draft-handled`;
17
21
  }
18
22
 
23
+ export function clearPersistedDraft(
24
+ storage: RemovableAnnotationStorage,
25
+ sessionId: string,
26
+ revision: number,
27
+ ): void {
28
+ try {
29
+ storage.removeItem(storageKey(sessionId, revision));
30
+ storage.removeItem(handledKey(sessionId, revision));
31
+ } catch {}
32
+ }
33
+
19
34
  function parseState(raw: string | null): object | null {
20
35
  if (!raw) return null;
21
36
  try {
@@ -118,6 +118,47 @@ export function uploadUrl(): string {
118
118
  return `/api/session/${MODE.sessionId}/upload`;
119
119
  }
120
120
 
121
+ function localSecretUrl(fieldId: string, suffix: "status" | "value"): string {
122
+ if (MODE.isShared) throw new Error("Secret inputs are available only on the local canvas daemon");
123
+ return `/api/session/${encodeURIComponent(MODE.sessionId)}/secrets/${encodeURIComponent(fieldId)}/${suffix}`;
124
+ }
125
+
126
+ async function secretApiError(response: Response): Promise<Error> {
127
+ const raw: unknown = await response.json().catch(() => null);
128
+ if (typeof raw === "object" && raw !== null && "error" in raw && typeof raw.error === "string") {
129
+ return new Error(raw.error);
130
+ }
131
+ return new Error(`Secret API request failed with status ${response.status}`);
132
+ }
133
+
134
+ /** Return only readiness metadata. The secret value is never sent back to the browser. */
135
+ export async function getLocalSecretStatus(fieldId: string): Promise<boolean> {
136
+ const response = await fetch(localSecretUrl(fieldId, "status"), { cache: "no-store" });
137
+ if (!response.ok) throw await secretApiError(response);
138
+ const raw: unknown = await response.json();
139
+ return typeof raw === "object" && raw !== null && "ready" in raw && raw.ready === true;
140
+ }
141
+
142
+ /** Deposit a secret into the daemon's in-memory vault. */
143
+ export async function storeLocalSecret(fieldId: string, value: string): Promise<void> {
144
+ const response = await fetch(localSecretUrl(fieldId, "value"), {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ cache: "no-store",
148
+ body: JSON.stringify({ value }),
149
+ });
150
+ if (!response.ok) throw await secretApiError(response);
151
+ }
152
+
153
+ /** Remove a deposited secret without retrieving its value. */
154
+ export async function clearLocalSecret(fieldId: string): Promise<void> {
155
+ const response = await fetch(localSecretUrl(fieldId, "value"), {
156
+ method: "DELETE",
157
+ cache: "no-store",
158
+ });
159
+ if (!response.ok) throw await secretApiError(response);
160
+ }
161
+
121
162
  // --- Meta fetch (with optional decryption) ---------------------------------
122
163
 
123
164
  interface RawMetaResponse {
@@ -0,0 +1,156 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useFeedback } from "#canvas/runtime";
3
+ import { MODE, clearLocalSecret, getLocalSecretStatus, storeLocalSecret } from "../clientApi";
4
+
5
+ const FIELD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7
+
8
+ interface SecretInputProps {
9
+ id: string;
10
+ label: string;
11
+ env: string;
12
+ placeholder?: string;
13
+ required?: boolean;
14
+ }
15
+
16
+ /** Local-only secret handoff that never enters annotation or feedback state. */
17
+ export function SecretInput({ id, label, env, placeholder, required }: SecretInputProps) {
18
+ const [value, setValue] = useState("");
19
+ const [ready, setReady] = useState(false);
20
+ const [busy, setBusy] = useState(!MODE.isShared);
21
+ const [error, setError] = useState<string | null>(null);
22
+ const validFieldId = FIELD_ID_RE.test(id);
23
+ const validEnvName = ENV_NAME_RE.test(env);
24
+ const locallyAvailable = !MODE.isShared;
25
+
26
+ useEffect(() => {
27
+ if (!locallyAvailable || !validFieldId) {
28
+ setBusy(false);
29
+ return;
30
+ }
31
+
32
+ let active = true;
33
+ getLocalSecretStatus(id)
34
+ .then((isReady) => {
35
+ if (active) setReady(isReady);
36
+ })
37
+ .catch((cause: unknown) => {
38
+ if (active) setError(cause instanceof Error ? cause.message : "Could not read secret status");
39
+ })
40
+ .finally(() => {
41
+ if (active) setBusy(false);
42
+ });
43
+ return () => { active = false; };
44
+ }, [id, locallyAvailable, validFieldId]);
45
+
46
+ const feedback = ready
47
+ ? `**${label}**\nSecret is ready. Field: \`${id}\`. Environment variable: \`${env}\`.\n`
48
+ : "";
49
+ useFeedback(`secret:${id}`, feedback, {
50
+ label,
51
+ required: required && locallyAvailable,
52
+ });
53
+
54
+ const store = async () => {
55
+ if (!value || !validFieldId || !validEnvName) return;
56
+ setBusy(true);
57
+ setError(null);
58
+ try {
59
+ await storeLocalSecret(id, value);
60
+ setValue("");
61
+ setReady(true);
62
+ } catch (cause: unknown) {
63
+ setError(cause instanceof Error ? cause.message : "Could not store secret");
64
+ } finally {
65
+ setBusy(false);
66
+ }
67
+ };
68
+
69
+ const clear = async () => {
70
+ setBusy(true);
71
+ setError(null);
72
+ try {
73
+ await clearLocalSecret(id);
74
+ setReady(false);
75
+ } catch (cause: unknown) {
76
+ setError(cause instanceof Error ? cause.message : "Could not clear secret");
77
+ } finally {
78
+ setBusy(false);
79
+ }
80
+ };
81
+
82
+ if (!locallyAvailable) {
83
+ return (
84
+ <div data-md="secretinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg bg-accent-amber-muted">
85
+ <div className="text-[13px] font-body font-medium text-text-primary">{label}</div>
86
+ <p className="text-[12px] font-body text-text-secondary mt-1">
87
+ Secret input is unavailable in shared canvases. Open this runbook on the author's local canvas daemon.
88
+ </p>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ const configurationError = !validFieldId
94
+ ? "Secret field IDs may contain letters, numbers, dots, underscores, and hyphens."
95
+ : !validEnvName
96
+ ? "Environment variable names must use letters, numbers, and underscores and may not start with a number."
97
+ : null;
98
+
99
+ return (
100
+ <div data-md="secretinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
101
+ <div className="flex items-baseline justify-between gap-3 mb-2">
102
+ <div className="flex items-baseline gap-2">
103
+ <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
104
+ {required && <span className="text-[10px] text-accent-red font-body">*</span>}
105
+ </div>
106
+ <span className={`text-[11px] font-body ${ready ? "text-accent-green" : "text-text-tertiary"}`}>
107
+ {busy ? "Working…" : ready ? "Ready in memory" : "Not stored"}
108
+ </span>
109
+ </div>
110
+
111
+ <div className="flex gap-2">
112
+ <input
113
+ type="password"
114
+ value={value}
115
+ onChange={(event) => setValue(event.target.value)}
116
+ onKeyDown={(event) => {
117
+ if (event.key === "Enter") {
118
+ event.preventDefault();
119
+ void store();
120
+ }
121
+ }}
122
+ placeholder={placeholder || "Paste secret value"}
123
+ autoComplete="off"
124
+ spellCheck={false}
125
+ disabled={busy || !!configurationError}
126
+ className="min-w-0 flex-1 bg-bg-input text-[13px] font-mono text-text-primary rounded-lg px-3 py-2 focus:outline-none border border-border-subtle focus:border-border-hover placeholder:text-text-tertiary disabled:opacity-50 transition-colors"
127
+ />
128
+ <button
129
+ type="button"
130
+ onClick={() => void store()}
131
+ disabled={busy || !value || !!configurationError}
132
+ className="px-3 py-2 rounded-lg bg-btn-primary text-btn-primary-text text-[12px] font-body font-medium disabled:opacity-40 disabled:cursor-default"
133
+ >
134
+ {ready ? "Replace" : "Store"}
135
+ </button>
136
+ {ready && (
137
+ <button
138
+ type="button"
139
+ onClick={() => void clear()}
140
+ disabled={busy}
141
+ className="px-3 py-2 rounded-lg border border-border-subtle text-text-secondary text-[12px] font-body font-medium hover:border-border-hover disabled:opacity-40"
142
+ >
143
+ Clear
144
+ </button>
145
+ )}
146
+ </div>
147
+
148
+ <p className="text-[11px] font-body text-text-tertiary mt-1.5">
149
+ Stored only in daemon memory. The agent receives field {id} for {env}, not the value.
150
+ </p>
151
+ {(configurationError || error) && (
152
+ <p className="text-[11px] text-accent-red font-body mt-1">{configurationError || error}</p>
153
+ )}
154
+ </div>
155
+ );
156
+ }
@@ -11,6 +11,7 @@ export { Mermaid } from "./Mermaid";
11
11
  export { Diff } from "./Diff";
12
12
  export { Choice, MultiChoice } from "./Choice";
13
13
  export { UserInput, RangeInput } from "./UserInput";
14
+ export { SecretInput } from "./SecretInput";
14
15
  export { ImageView } from "./Image";
15
16
  export { Markdown } from "./Markdown";
16
17
  export { useFeedback } from "#canvas/runtime";