@tangle-network/agent-app 0.30.0 → 0.32.0
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/dist/IntakeInterview-WHVKMNQW.js +8 -0
- package/dist/IntakeInterview-WHVKMNQW.js.map +1 -0
- package/dist/VaultPane-2BRLHKEB.js +7 -0
- package/dist/VaultPane-2BRLHKEB.js.map +1 -0
- package/dist/VaultPane-B8ipCqkB.d.ts +118 -0
- package/dist/chunk-4K7BZYO7.js +124 -0
- package/dist/chunk-4K7BZYO7.js.map +1 -0
- package/dist/chunk-5I72FRZY.js +212 -0
- package/dist/chunk-5I72FRZY.js.map +1 -0
- package/dist/{chunk-WOVCWPFF.js → chunk-FJUOBNIY.js} +1 -1
- package/dist/chunk-ND3XEGBE.js +119 -0
- package/dist/chunk-ND3XEGBE.js.map +1 -0
- package/dist/chunk-SMVD76VC.js +584 -0
- package/dist/chunk-SMVD76VC.js.map +1 -0
- package/dist/chunk-USYXHDKE.js +30 -0
- package/dist/chunk-USYXHDKE.js.map +1 -0
- package/dist/design-canvas-react/index.js +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/intakes/api.d.ts +59 -0
- package/dist/intakes/api.js +63 -0
- package/dist/intakes/api.js.map +1 -0
- package/dist/intakes/drizzle.d.ts +385 -0
- package/dist/intakes/drizzle.js +61 -0
- package/dist/intakes/drizzle.js.map +1 -0
- package/dist/intakes/index.d.ts +42 -0
- package/dist/intakes/index.js +31 -0
- package/dist/intakes/index.js.map +1 -0
- package/dist/intakes-react/index.d.ts +48 -0
- package/dist/intakes-react/index.js +8 -0
- package/dist/intakes-react/index.js.map +1 -0
- package/dist/intakes-react/lazy.d.ts +8 -0
- package/dist/intakes-react/lazy.js +9 -0
- package/dist/intakes-react/lazy.js.map +1 -0
- package/dist/model-Cuj7d-xT.d.ts +116 -0
- package/dist/vault/index.d.ts +22 -0
- package/dist/vault/index.js +9 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/lazy.d.ts +7 -0
- package/dist/vault/lazy.js +9 -0
- package/dist/vault/lazy.js.map +1 -0
- package/package.json +36 -1
- /package/dist/{chunk-WOVCWPFF.js.map → chunk-FJUOBNIY.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/intakes/model.ts"],"sourcesContent":["/**\n * Pure intake MODEL — the question-graph and the answer/completion algebra a\n * one-question-at-a-time interview runs on. Zero dependencies: no drizzle, no\n * env, no react, no I/O. The DB layer (`./intakes/drizzle`), the handlers\n * (`./intakes/api`) and the React surface (`./intakes-react`) all build on\n * these functions; this leaf imports nothing back, so a consumer can pull just\n * the intake math.\n *\n * An intake is an ordered list of questions, optionally branching: a question\n * can declare `next` as a function of the answers so far, so the next prompt\n * depends on what was said (e.g. \"do you have a website?\" → no → skip the URL\n * question). Traversal is pure and deterministic — `nextQuestion(graph, answers)`\n * folds the answers into the single question to ask next (or null = done), and\n * `isComplete` is true exactly when every REQUIRED reachable question has a\n * valid answer.\n *\n * Answers are a flat `Record<questionId, value>`. Each question declares an\n * answer `type` that `validateAnswer` checks against — the same validation the\n * UI runs before advancing and the store runs before persisting, so an invalid\n * answer can never enter the payload.\n */\n\n/** The kinds of answer a question accepts. */\nexport type IntakeAnswerType =\n | 'text'\n | 'long-text'\n | 'single-select'\n | 'multi-select'\n | 'boolean'\n | 'number'\n | 'url'\n | 'email'\n\n/** A selectable option for single/multi-select questions. */\nexport interface IntakeOption {\n value: string\n label: string\n}\n\n/** A flat map of answers keyed by question id. */\nexport type IntakeAnswers = Record<string, IntakeAnswerValue>\n\n/** Any value an answer can hold; the type is validated per-question. */\nexport type IntakeAnswerValue = string | string[] | number | boolean | null\n\n/**\n * One question in the graph. `next` (optional) makes the graph branch: given\n * the answers so far it returns the id of the question to ask next, or null to\n * end the interview early. With no `next`, traversal falls through to the next\n * question in declaration order.\n */\nexport interface IntakeQuestion {\n id: string\n /** The prompt the interviewer asks. */\n prompt: string\n type: IntakeAnswerType\n /** Required questions gate completion; optional ones may be skipped. */\n required?: boolean\n /** Help text shown under the prompt. */\n help?: string\n /** Options for single/multi-select questions. */\n options?: IntakeOption[]\n /** Min length (text) or min value (number); inclusive. */\n min?: number\n /** Max length (text) or max value (number); inclusive. */\n max?: number\n /**\n * Branch override: given the answers so far, the id of the next question\n * (or null to end early). Omit for linear flow (next in declaration order).\n */\n next?(answers: IntakeAnswers): string | null\n}\n\n/** The intake definition: an ordered, addressable set of questions. */\nexport interface IntakeGraph {\n /** Stable id for the intake definition (e.g. 'user-onboarding-v1'). */\n id: string\n /** Human title shown at the top of the interview. */\n title: string\n /** Optional one-line description. */\n description?: string\n /** Questions in declaration (default traversal) order. */\n questions: IntakeQuestion[]\n}\n\n/** The reason an answer failed validation. */\nexport type AnswerRejectionReason =\n | 'required'\n | 'wrong-type'\n | 'too-short'\n | 'too-long'\n | 'too-small'\n | 'too-large'\n | 'not-an-option'\n | 'invalid-url'\n | 'invalid-email'\n | 'unknown-question'\n\nexport interface AnswerValidationResult {\n ok: boolean\n reason?: AnswerRejectionReason\n}\n\nconst URL_PATTERN = /^https?:\\/\\/[^\\s/$.?#].[^\\s]*$/i\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Look up a question by id, or null if the graph has none. */\nexport function getQuestion(graph: IntakeGraph, questionId: string): IntakeQuestion | null {\n return graph.questions.find((q) => q.id === questionId) ?? null\n}\n\n/** True when an answer value is present (not null/undefined/empty). */\nexport function hasAnswer(value: IntakeAnswerValue | undefined): boolean {\n if (value == null) return false\n if (typeof value === 'string') return value.trim().length > 0\n if (Array.isArray(value)) return value.length > 0\n return true\n}\n\n/**\n * Validate one answer against its question. Pure: no I/O, no graph mutation.\n * An empty value is rejected as `required` only when the question is required;\n * an empty value for an optional question is OK (the user skipped it).\n */\nexport function validateAnswer(\n question: IntakeQuestion,\n value: IntakeAnswerValue | undefined,\n): AnswerValidationResult {\n if (!hasAnswer(value)) {\n return question.required ? { ok: false, reason: 'required' } : { ok: true }\n }\n\n switch (question.type) {\n case 'text':\n case 'long-text': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n const len = value.trim().length\n if (question.min != null && len < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && len > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n case 'url': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return URL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-url' }\n }\n case 'email': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return EMAIL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-email' }\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) return { ok: false, reason: 'wrong-type' }\n if (question.min != null && value < question.min) return { ok: false, reason: 'too-small' }\n if (question.max != null && value > question.max) return { ok: false, reason: 'too-large' }\n return { ok: true }\n }\n case 'boolean': {\n return typeof value === 'boolean' ? { ok: true } : { ok: false, reason: 'wrong-type' }\n }\n case 'single-select': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return optionExists(question, value) ? { ok: true } : { ok: false, reason: 'not-an-option' }\n }\n case 'multi-select': {\n if (!Array.isArray(value)) return { ok: false, reason: 'wrong-type' }\n if (!value.every((v) => typeof v === 'string' && optionExists(question, v))) {\n return { ok: false, reason: 'not-an-option' }\n }\n if (question.min != null && value.length < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && value.length > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n default:\n return { ok: false, reason: 'wrong-type' }\n }\n}\n\n/**\n * The single question to ask next given the answers so far, or null when the\n * interview is done. Walks the graph from the first question, following each\n * question's `next` branch (or declaration order) and stopping at the first\n * reachable question that has no valid answer yet. Deterministic and pure.\n *\n * A `next` that points at a missing id, or returns null, ends the walk — so a\n * malformed graph terminates rather than looping. A visited-set guards against\n * a cyclic `next`.\n */\nexport function nextQuestion(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion | null {\n if (graph.questions.length === 0) return null\n const visited = new Set<string>()\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) return null\n visited.add(current.id)\n\n const answer = answers[current.id]\n const validity = validateAnswer(current, answer)\n if (!validity.ok || (current.required && !hasAnswer(answer))) {\n return current\n }\n\n current = advance(graph, current, answers)\n }\n\n return null\n}\n\n/**\n * True when every REQUIRED question reachable under the current answers has a\n * valid answer. Pure: it replays the same traversal `nextQuestion` uses and is\n * complete exactly when that traversal has no question left to ask.\n */\nexport function isComplete(graph: IntakeGraph, answers: IntakeAnswers): boolean {\n return nextQuestion(graph, answers) === null\n}\n\n/**\n * Progress as answered-vs-total over the REACHABLE required questions under the\n * current answers — what a progress bar renders. Optional questions don't count\n * toward the denominator (they never block completion).\n */\nexport function intakeProgress(graph: IntakeGraph, answers: IntakeAnswers): { answered: number; total: number } {\n const reachable = reachableQuestions(graph, answers)\n const required = reachable.filter((q) => q.required)\n const answered = required.filter((q) => validateAnswer(q, answers[q.id]).ok && hasAnswer(answers[q.id])).length\n return { answered, total: required.length }\n}\n\n/** The questions reachable under the current answers, in traversal order. */\nexport function reachableQuestions(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion[] {\n if (graph.questions.length === 0) return []\n const visited = new Set<string>()\n const out: IntakeQuestion[] = []\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) break\n visited.add(current.id)\n out.push(current)\n current = advance(graph, current, answers)\n }\n return out\n}\n\nfunction advance(graph: IntakeGraph, current: IntakeQuestion, answers: IntakeAnswers): IntakeQuestion | null {\n if (current.next) {\n const nextId = current.next(answers)\n return nextId == null ? null : getQuestion(graph, nextId)\n }\n const index = graph.questions.indexOf(current)\n return graph.questions[index + 1] ?? null\n}\n\nfunction optionExists(question: IntakeQuestion, value: string): boolean {\n return (question.options ?? []).some((option) => option.value === value)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// src/vault/VaultPane.tsx
|
|
2
|
+
import {
|
|
3
|
+
Component,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect as useEffect2,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef as useRef2,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// src/vault/ConfirmDialog.tsx
|
|
12
|
+
import { useEffect, useRef } from "react";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function ConfirmDialog({
|
|
15
|
+
open,
|
|
16
|
+
title,
|
|
17
|
+
description,
|
|
18
|
+
confirmLabel = "Confirm",
|
|
19
|
+
cancelLabel = "Cancel",
|
|
20
|
+
destructive = false,
|
|
21
|
+
confirmDisabled = false,
|
|
22
|
+
onConfirm,
|
|
23
|
+
onCancel,
|
|
24
|
+
children
|
|
25
|
+
}) {
|
|
26
|
+
const panelRef = useRef(null);
|
|
27
|
+
const confirmRef = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!open) return;
|
|
30
|
+
confirmRef.current?.focus();
|
|
31
|
+
}, [open]);
|
|
32
|
+
if (!open) return null;
|
|
33
|
+
function onKeyDown(event) {
|
|
34
|
+
if (event.key === "Escape") {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onCancel();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (event.key === "Enter" && !confirmDisabled) {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
onConfirm();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (event.key !== "Tab") return;
|
|
45
|
+
const focusable = panelRef.current?.querySelectorAll(
|
|
46
|
+
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
47
|
+
);
|
|
48
|
+
if (!focusable || focusable.length === 0) return;
|
|
49
|
+
const first = focusable[0];
|
|
50
|
+
const last = focusable[focusable.length - 1];
|
|
51
|
+
const active = document.activeElement;
|
|
52
|
+
if (event.shiftKey && active === first) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
last?.focus();
|
|
55
|
+
} else if (!event.shiftKey && active === last) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
first?.focus();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return /* @__PURE__ */ jsx(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4",
|
|
64
|
+
onMouseDown: (e) => {
|
|
65
|
+
if (e.target === e.currentTarget) onCancel();
|
|
66
|
+
},
|
|
67
|
+
children: /* @__PURE__ */ jsxs(
|
|
68
|
+
"div",
|
|
69
|
+
{
|
|
70
|
+
ref: panelRef,
|
|
71
|
+
role: "dialog",
|
|
72
|
+
"aria-modal": "true",
|
|
73
|
+
"aria-label": title,
|
|
74
|
+
onKeyDown,
|
|
75
|
+
className: "w-full max-w-md rounded-lg border border-border bg-card p-5 shadow-lg",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: title }),
|
|
78
|
+
description && /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-xs text-muted-foreground", children: description }),
|
|
79
|
+
children && /* @__PURE__ */ jsx("div", { className: "mt-3", children }),
|
|
80
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 flex justify-end gap-2", children: [
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"button",
|
|
83
|
+
{
|
|
84
|
+
type: "button",
|
|
85
|
+
onClick: onCancel,
|
|
86
|
+
className: "inline-flex h-8 items-center rounded-md px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60",
|
|
87
|
+
children: cancelLabel
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
"button",
|
|
92
|
+
{
|
|
93
|
+
ref: confirmRef,
|
|
94
|
+
type: "button",
|
|
95
|
+
onClick: onConfirm,
|
|
96
|
+
disabled: confirmDisabled,
|
|
97
|
+
className: `inline-flex h-8 items-center rounded-md px-3 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60 disabled:pointer-events-none disabled:opacity-50 ${destructive ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "bg-primary text-primary-foreground hover:bg-primary/90"}`,
|
|
98
|
+
children: confirmLabel
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] })
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/vault/VaultPane.tsx
|
|
110
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
111
|
+
var IDENTITY_CODEC = {
|
|
112
|
+
parse: (raw) => raw,
|
|
113
|
+
serialize: (parts) => typeof parts === "string" ? parts : String(parts ?? "")
|
|
114
|
+
};
|
|
115
|
+
function collectFilePaths(nodes, into) {
|
|
116
|
+
for (const node of nodes) {
|
|
117
|
+
if (node.type === "file") into.add(node.path);
|
|
118
|
+
if (node.children) collectFilePaths(node.children, into);
|
|
119
|
+
}
|
|
120
|
+
return into;
|
|
121
|
+
}
|
|
122
|
+
function countFiles(nodes) {
|
|
123
|
+
return nodes.reduce(
|
|
124
|
+
(sum, node) => node.type === "file" ? sum + 1 : sum + countFiles(node.children ?? []),
|
|
125
|
+
0
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
var EditorErrorBoundary = class extends Component {
|
|
129
|
+
state = { error: null };
|
|
130
|
+
static getDerivedStateFromError(error) {
|
|
131
|
+
return { error };
|
|
132
|
+
}
|
|
133
|
+
componentDidCatch(error, info) {
|
|
134
|
+
console.error("Vault crashed:", error, info);
|
|
135
|
+
}
|
|
136
|
+
render() {
|
|
137
|
+
if (this.state.error) {
|
|
138
|
+
const msg = this.state.error instanceof Error ? this.state.error.message : typeof this.state.error === "string" ? this.state.error : "Something went wrong loading the vault";
|
|
139
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-1 flex-col items-center justify-center p-8 text-center", children: [
|
|
140
|
+
/* @__PURE__ */ jsx2("h3", { className: "mb-1 text-sm font-medium text-foreground", children: "Vault failed to load" }),
|
|
141
|
+
/* @__PURE__ */ jsx2("p", { className: "mb-4 max-w-xs text-xs text-muted-foreground", children: String(msg) }),
|
|
142
|
+
/* @__PURE__ */ jsx2(
|
|
143
|
+
"button",
|
|
144
|
+
{
|
|
145
|
+
type: "button",
|
|
146
|
+
onClick: () => {
|
|
147
|
+
this.setState({ error: null });
|
|
148
|
+
this.props.onReset?.();
|
|
149
|
+
},
|
|
150
|
+
className: "inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60",
|
|
151
|
+
children: "Try again"
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
return this.props.children;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function TreeSkeleton() {
|
|
160
|
+
return /* @__PURE__ */ jsx2("div", { className: "space-y-2 p-4", "aria-hidden": "true", children: [32, 48, 40, 52].map((w, i) => /* @__PURE__ */ jsx2("div", { className: "h-4 animate-pulse rounded bg-muted", style: { width: `${w * 4}px` } }, i)) });
|
|
161
|
+
}
|
|
162
|
+
function EditorSkeleton() {
|
|
163
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-3 p-8", "aria-hidden": "true", children: [
|
|
164
|
+
/* @__PURE__ */ jsx2("div", { className: "h-5 w-1/2 animate-pulse rounded bg-muted" }),
|
|
165
|
+
/* @__PURE__ */ jsx2("div", { className: "h-4 w-full animate-pulse rounded bg-muted" }),
|
|
166
|
+
/* @__PURE__ */ jsx2("div", { className: "h-4 w-3/4 animate-pulse rounded bg-muted" }),
|
|
167
|
+
/* @__PURE__ */ jsx2("div", { className: "h-4 w-2/3 animate-pulse rounded bg-muted" })
|
|
168
|
+
] });
|
|
169
|
+
}
|
|
170
|
+
function EmptyState() {
|
|
171
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col items-center justify-center p-8 text-center", children: [
|
|
172
|
+
/* @__PURE__ */ jsx2("h3", { className: "text-sm font-medium text-foreground", children: "Open a vault document" }),
|
|
173
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-1 max-w-xs text-xs text-muted-foreground", children: "Select a file from the directory, or create a new one." })
|
|
174
|
+
] });
|
|
175
|
+
}
|
|
176
|
+
function VaultPane(props) {
|
|
177
|
+
const {
|
|
178
|
+
port,
|
|
179
|
+
renderTree,
|
|
180
|
+
renderArtifact,
|
|
181
|
+
renderDock,
|
|
182
|
+
canWrite = true,
|
|
183
|
+
selectedPath: controlledPath,
|
|
184
|
+
onSelectedPathChange,
|
|
185
|
+
codec,
|
|
186
|
+
className
|
|
187
|
+
} = props;
|
|
188
|
+
const activeCodec = codec ?? IDENTITY_CODEC;
|
|
189
|
+
const controlled = controlledPath !== void 0;
|
|
190
|
+
const isMarkdownCapable = codec !== void 0;
|
|
191
|
+
const [tree, setTree] = useState([]);
|
|
192
|
+
const [treeLoading, setTreeLoading] = useState(true);
|
|
193
|
+
const [internalPath, setInternalPath] = useState(null);
|
|
194
|
+
const selectedPath = controlled ? controlledPath ?? null : internalPath;
|
|
195
|
+
const [selectedFile, setSelectedFile] = useState(null);
|
|
196
|
+
const [fileLoading, setFileLoading] = useState(false);
|
|
197
|
+
const [saving, setSaving] = useState(false);
|
|
198
|
+
const [editorMode, setEditorMode] = useState("rich");
|
|
199
|
+
const [richDraft, setRichDraft] = useState("");
|
|
200
|
+
const [sourceDraft, setSourceDraft] = useState("");
|
|
201
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
202
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
203
|
+
const [newPath, setNewPath] = useState("");
|
|
204
|
+
const [creating, setCreating] = useState(false);
|
|
205
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
206
|
+
const [deleting, setDeleting] = useState(false);
|
|
207
|
+
const [dockOpen, setDockOpen] = useState(false);
|
|
208
|
+
const [pendingNav, setPendingNav] = useState(null);
|
|
209
|
+
const savedContentRef = useRef2("");
|
|
210
|
+
const loadedPathRef = useRef2(null);
|
|
211
|
+
const filePaths = useMemo(() => collectFilePaths(tree, /* @__PURE__ */ new Set()), [tree]);
|
|
212
|
+
const treeRoot = useMemo(
|
|
213
|
+
() => ({ name: "Vault", path: "", type: "directory", children: tree }),
|
|
214
|
+
[tree]
|
|
215
|
+
);
|
|
216
|
+
const fileCount = useMemo(() => countFiles(tree), [tree]);
|
|
217
|
+
const commitPath = useCallback(
|
|
218
|
+
(next) => {
|
|
219
|
+
if (!controlled) setInternalPath(next);
|
|
220
|
+
onSelectedPathChange?.(next);
|
|
221
|
+
},
|
|
222
|
+
[controlled, onSelectedPathChange]
|
|
223
|
+
);
|
|
224
|
+
const refresh = useCallback(async () => {
|
|
225
|
+
setTreeLoading(true);
|
|
226
|
+
try {
|
|
227
|
+
setTree(await port.listTree());
|
|
228
|
+
} finally {
|
|
229
|
+
setTreeLoading(false);
|
|
230
|
+
}
|
|
231
|
+
}, [port]);
|
|
232
|
+
useEffect2(() => {
|
|
233
|
+
void refresh();
|
|
234
|
+
}, [refresh]);
|
|
235
|
+
useEffect2(() => {
|
|
236
|
+
if (!selectedPath) {
|
|
237
|
+
setSelectedFile(null);
|
|
238
|
+
setFileLoading(false);
|
|
239
|
+
loadedPathRef.current = null;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
let cancelled = false;
|
|
243
|
+
const path = selectedPath;
|
|
244
|
+
setFileLoading(true);
|
|
245
|
+
void (async () => {
|
|
246
|
+
try {
|
|
247
|
+
const file = await port.readFile(path);
|
|
248
|
+
if (!cancelled) setSelectedFile(file);
|
|
249
|
+
} catch {
|
|
250
|
+
if (!cancelled) setSelectedFile(null);
|
|
251
|
+
} finally {
|
|
252
|
+
if (!cancelled) setFileLoading(false);
|
|
253
|
+
}
|
|
254
|
+
})();
|
|
255
|
+
return () => {
|
|
256
|
+
cancelled = true;
|
|
257
|
+
};
|
|
258
|
+
}, [port, selectedPath]);
|
|
259
|
+
useEffect2(() => {
|
|
260
|
+
if (!selectedFile) {
|
|
261
|
+
loadedPathRef.current = null;
|
|
262
|
+
savedContentRef.current = "";
|
|
263
|
+
setRichDraft("");
|
|
264
|
+
setSourceDraft("");
|
|
265
|
+
setEditorMode("rich");
|
|
266
|
+
setIsDirty(false);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const pathChanged = loadedPathRef.current !== selectedFile.path;
|
|
270
|
+
loadedPathRef.current = selectedFile.path;
|
|
271
|
+
savedContentRef.current = selectedFile.content;
|
|
272
|
+
setRichDraft(activeCodec.parse(selectedFile.content));
|
|
273
|
+
setSourceDraft(selectedFile.content);
|
|
274
|
+
if (pathChanged) setEditorMode("rich");
|
|
275
|
+
setIsDirty(false);
|
|
276
|
+
setDockOpen(false);
|
|
277
|
+
}, [selectedFile, activeCodec]);
|
|
278
|
+
const guardedOpen = useCallback(
|
|
279
|
+
(path) => {
|
|
280
|
+
if (path === selectedPath) return;
|
|
281
|
+
if (isDirty) {
|
|
282
|
+
setPendingNav({ type: "open", path });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
commitPath(path);
|
|
286
|
+
},
|
|
287
|
+
[isDirty, selectedPath, commitPath]
|
|
288
|
+
);
|
|
289
|
+
const guardedClose = useCallback(() => {
|
|
290
|
+
if (isDirty) {
|
|
291
|
+
setPendingNav({ type: "close" });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
commitPath(null);
|
|
295
|
+
setSelectedFile(null);
|
|
296
|
+
}, [isDirty, commitPath]);
|
|
297
|
+
const confirmDiscard = useCallback(() => {
|
|
298
|
+
const nav = pendingNav;
|
|
299
|
+
setPendingNav(null);
|
|
300
|
+
setIsDirty(false);
|
|
301
|
+
if (!nav) return;
|
|
302
|
+
if (nav.type === "open") {
|
|
303
|
+
commitPath(nav.path);
|
|
304
|
+
} else {
|
|
305
|
+
commitPath(null);
|
|
306
|
+
setSelectedFile(null);
|
|
307
|
+
}
|
|
308
|
+
}, [pendingNav, commitPath]);
|
|
309
|
+
const showRichMode = useCallback(() => {
|
|
310
|
+
setEditorMode((mode) => {
|
|
311
|
+
if (mode === "rich") return mode;
|
|
312
|
+
setRichDraft(activeCodec.parse(sourceDraft));
|
|
313
|
+
setIsDirty(sourceDraft !== savedContentRef.current);
|
|
314
|
+
return "rich";
|
|
315
|
+
});
|
|
316
|
+
}, [activeCodec, sourceDraft]);
|
|
317
|
+
const showSourceMode = useCallback(() => {
|
|
318
|
+
setEditorMode((mode) => {
|
|
319
|
+
if (mode === "source") return mode;
|
|
320
|
+
const content = isDirty ? activeCodec.serialize(richDraft) : savedContentRef.current;
|
|
321
|
+
setSourceDraft(content);
|
|
322
|
+
setIsDirty(content !== savedContentRef.current);
|
|
323
|
+
return "source";
|
|
324
|
+
});
|
|
325
|
+
}, [activeCodec, isDirty, richDraft]);
|
|
326
|
+
const onSourceChange = useCallback((next) => {
|
|
327
|
+
setSourceDraft(next);
|
|
328
|
+
setIsDirty(next !== savedContentRef.current);
|
|
329
|
+
}, []);
|
|
330
|
+
const saveCurrent = useCallback(async () => {
|
|
331
|
+
if (!selectedFile) return;
|
|
332
|
+
const content = editorMode === "source" ? sourceDraft : activeCodec.serialize(richDraft);
|
|
333
|
+
setSaving(true);
|
|
334
|
+
try {
|
|
335
|
+
await port.writeFile(selectedFile.path, content);
|
|
336
|
+
savedContentRef.current = content;
|
|
337
|
+
setSelectedFile({ ...selectedFile, content });
|
|
338
|
+
setSourceDraft(content);
|
|
339
|
+
setRichDraft(activeCodec.parse(content));
|
|
340
|
+
setIsDirty(false);
|
|
341
|
+
} finally {
|
|
342
|
+
setSaving(false);
|
|
343
|
+
}
|
|
344
|
+
}, [selectedFile, editorMode, sourceDraft, richDraft, activeCodec, port]);
|
|
345
|
+
const handleCreate = useCallback(async () => {
|
|
346
|
+
const trimmed = newPath.trim();
|
|
347
|
+
if (!trimmed) return;
|
|
348
|
+
setCreating(true);
|
|
349
|
+
try {
|
|
350
|
+
await port.createFile(trimmed);
|
|
351
|
+
setCreateOpen(false);
|
|
352
|
+
setNewPath("");
|
|
353
|
+
await refresh();
|
|
354
|
+
commitPath(trimmed);
|
|
355
|
+
} finally {
|
|
356
|
+
setCreating(false);
|
|
357
|
+
}
|
|
358
|
+
}, [newPath, port, refresh, commitPath]);
|
|
359
|
+
const handleDelete = useCallback(async () => {
|
|
360
|
+
if (!selectedFile) return;
|
|
361
|
+
setDeleting(true);
|
|
362
|
+
try {
|
|
363
|
+
await port.deleteFile(selectedFile.path);
|
|
364
|
+
setDeleteOpen(false);
|
|
365
|
+
setIsDirty(false);
|
|
366
|
+
commitPath(null);
|
|
367
|
+
setSelectedFile(null);
|
|
368
|
+
await refresh();
|
|
369
|
+
} finally {
|
|
370
|
+
setDeleting(false);
|
|
371
|
+
}
|
|
372
|
+
}, [selectedFile, port, refresh, commitPath]);
|
|
373
|
+
return /* @__PURE__ */ jsx2(EditorErrorBoundary, { onReset: () => {
|
|
374
|
+
commitPath(null);
|
|
375
|
+
setSelectedFile(null);
|
|
376
|
+
}, children: /* @__PURE__ */ jsxs2("div", { className: `flex min-h-0 flex-1 overflow-hidden ${className ?? ""}`, children: [
|
|
377
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex w-[23rem] min-w-[23rem] flex-col border-r border-border bg-background", children: [
|
|
378
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-3", children: [
|
|
379
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 items-center gap-2", children: [
|
|
380
|
+
/* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold text-foreground", children: "Vault" }),
|
|
381
|
+
/* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground", children: [
|
|
382
|
+
fileCount,
|
|
383
|
+
" file",
|
|
384
|
+
fileCount === 1 ? "" : "s"
|
|
385
|
+
] })
|
|
386
|
+
] }),
|
|
387
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
388
|
+
/* @__PURE__ */ jsx2(
|
|
389
|
+
"button",
|
|
390
|
+
{
|
|
391
|
+
type: "button",
|
|
392
|
+
"aria-label": "Refresh vault",
|
|
393
|
+
onClick: () => void refresh(),
|
|
394
|
+
className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60",
|
|
395
|
+
children: "\u21BB"
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
canWrite && /* @__PURE__ */ jsx2(
|
|
399
|
+
"button",
|
|
400
|
+
{
|
|
401
|
+
type: "button",
|
|
402
|
+
"aria-label": "New vault file",
|
|
403
|
+
onClick: () => setCreateOpen(true),
|
|
404
|
+
className: "inline-flex h-8 w-8 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60",
|
|
405
|
+
children: "+"
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
] })
|
|
409
|
+
] }),
|
|
410
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 overflow-y-auto", children: treeLoading ? /* @__PURE__ */ jsx2(TreeSkeleton, {}) : renderTree({
|
|
411
|
+
root: treeRoot,
|
|
412
|
+
selectedPath: selectedPath ?? void 0,
|
|
413
|
+
onSelect: (path) => {
|
|
414
|
+
if (filePaths.has(path)) guardedOpen(path);
|
|
415
|
+
}
|
|
416
|
+
}) })
|
|
417
|
+
] }),
|
|
418
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [
|
|
419
|
+
selectedFile && /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center justify-between border-b border-border bg-card px-4 py-1.5", children: [
|
|
420
|
+
/* @__PURE__ */ jsx2("span", { "data-vault-path": true, className: "truncate text-xs text-muted-foreground", children: selectedFile.path }),
|
|
421
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
|
|
422
|
+
canWrite && isMarkdownCapable && /* @__PURE__ */ jsxs2("div", { className: "mr-1 flex items-center gap-1", children: [
|
|
423
|
+
/* @__PURE__ */ jsx2(
|
|
424
|
+
"button",
|
|
425
|
+
{
|
|
426
|
+
type: "button",
|
|
427
|
+
"aria-label": "Edit as rich text",
|
|
428
|
+
"aria-pressed": editorMode === "rich",
|
|
429
|
+
onClick: showRichMode,
|
|
430
|
+
className: `inline-flex h-7 items-center rounded px-2 text-xs transition-colors ${editorMode === "rich" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
431
|
+
children: "Rich"
|
|
432
|
+
}
|
|
433
|
+
),
|
|
434
|
+
/* @__PURE__ */ jsx2(
|
|
435
|
+
"button",
|
|
436
|
+
{
|
|
437
|
+
type: "button",
|
|
438
|
+
"aria-label": "Edit as source",
|
|
439
|
+
"aria-pressed": editorMode === "source",
|
|
440
|
+
onClick: showSourceMode,
|
|
441
|
+
className: `inline-flex h-7 items-center rounded px-2 text-xs transition-colors ${editorMode === "source" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
442
|
+
children: "Source"
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
] }),
|
|
446
|
+
renderDock && /* @__PURE__ */ jsx2(
|
|
447
|
+
"button",
|
|
448
|
+
{
|
|
449
|
+
type: "button",
|
|
450
|
+
"aria-label": "Discuss with agent",
|
|
451
|
+
"aria-pressed": dockOpen,
|
|
452
|
+
disabled: isDirty,
|
|
453
|
+
title: isDirty ? "Save before discussing with agent" : "Discuss with agent",
|
|
454
|
+
onClick: () => setDockOpen((v) => !v),
|
|
455
|
+
className: `inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs transition-colors disabled:pointer-events-none disabled:opacity-40 ${dockOpen ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
456
|
+
children: "Discuss"
|
|
457
|
+
}
|
|
458
|
+
),
|
|
459
|
+
canWrite && /* @__PURE__ */ jsx2(
|
|
460
|
+
"button",
|
|
461
|
+
{
|
|
462
|
+
type: "button",
|
|
463
|
+
"aria-label": "Delete this file",
|
|
464
|
+
title: "Delete file",
|
|
465
|
+
onClick: () => setDeleteOpen(true),
|
|
466
|
+
className: "p-1 text-muted-foreground transition-colors hover:text-destructive",
|
|
467
|
+
children: "\u2715"
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
] })
|
|
471
|
+
] }),
|
|
472
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 overflow-hidden", children: fileLoading ? /* @__PURE__ */ jsx2(EditorSkeleton, {}) : selectedFile && canWrite && isMarkdownCapable && editorMode === "source" ? /* @__PURE__ */ jsx2(
|
|
473
|
+
SourceEditor,
|
|
474
|
+
{
|
|
475
|
+
path: selectedFile.path,
|
|
476
|
+
content: sourceDraft,
|
|
477
|
+
saving,
|
|
478
|
+
dirty: isDirty,
|
|
479
|
+
onChange: onSourceChange,
|
|
480
|
+
onSave: () => void saveCurrent()
|
|
481
|
+
}
|
|
482
|
+
) : selectedFile ? renderArtifact({ file: selectedFile, loading: false }) : /* @__PURE__ */ jsx2(EmptyState, {}) })
|
|
483
|
+
] }),
|
|
484
|
+
renderDock && selectedFile && renderDock({
|
|
485
|
+
file: selectedFile,
|
|
486
|
+
open: dockOpen,
|
|
487
|
+
onClose: () => setDockOpen(false)
|
|
488
|
+
}),
|
|
489
|
+
/* @__PURE__ */ jsx2(
|
|
490
|
+
ConfirmDialog,
|
|
491
|
+
{
|
|
492
|
+
open: createOpen,
|
|
493
|
+
title: "Create vault file",
|
|
494
|
+
description: "Add a new document to this vault.",
|
|
495
|
+
confirmLabel: creating ? "Creating\u2026" : "Create",
|
|
496
|
+
confirmDisabled: creating || !newPath.trim(),
|
|
497
|
+
onConfirm: () => void handleCreate(),
|
|
498
|
+
onCancel: () => {
|
|
499
|
+
setCreateOpen(false);
|
|
500
|
+
setNewPath("");
|
|
501
|
+
},
|
|
502
|
+
children: /* @__PURE__ */ jsx2(
|
|
503
|
+
"input",
|
|
504
|
+
{
|
|
505
|
+
value: newPath,
|
|
506
|
+
autoFocus: true,
|
|
507
|
+
onChange: (e) => setNewPath(e.target.value),
|
|
508
|
+
placeholder: "e.g. playbooks/new-strategy.md",
|
|
509
|
+
"aria-label": "New file path",
|
|
510
|
+
className: "h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-primary/60"
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
),
|
|
515
|
+
/* @__PURE__ */ jsx2(
|
|
516
|
+
ConfirmDialog,
|
|
517
|
+
{
|
|
518
|
+
open: deleteOpen,
|
|
519
|
+
title: "Delete file?",
|
|
520
|
+
description: `This permanently removes ${selectedFile?.path ?? "this file"} from the vault.`,
|
|
521
|
+
confirmLabel: deleting ? "Deleting\u2026" : "Delete file",
|
|
522
|
+
confirmDisabled: deleting,
|
|
523
|
+
destructive: true,
|
|
524
|
+
onConfirm: () => void handleDelete(),
|
|
525
|
+
onCancel: () => setDeleteOpen(false)
|
|
526
|
+
}
|
|
527
|
+
),
|
|
528
|
+
/* @__PURE__ */ jsx2(
|
|
529
|
+
ConfirmDialog,
|
|
530
|
+
{
|
|
531
|
+
open: pendingNav !== null,
|
|
532
|
+
title: "Discard unsaved changes?",
|
|
533
|
+
description: "Your edits to this document haven't been saved. Continue and lose them?",
|
|
534
|
+
confirmLabel: "Discard changes",
|
|
535
|
+
destructive: true,
|
|
536
|
+
onConfirm: confirmDiscard,
|
|
537
|
+
onCancel: () => setPendingNav(null)
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
] }) });
|
|
541
|
+
}
|
|
542
|
+
function SourceEditor({
|
|
543
|
+
path,
|
|
544
|
+
content,
|
|
545
|
+
saving,
|
|
546
|
+
dirty,
|
|
547
|
+
onChange,
|
|
548
|
+
onSave
|
|
549
|
+
}) {
|
|
550
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 flex-col bg-background", children: [
|
|
551
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 border-b border-border px-4 py-2", children: [
|
|
552
|
+
/* @__PURE__ */ jsx2("p", { className: "truncate font-mono text-[11px] text-muted-foreground", children: path }),
|
|
553
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
554
|
+
/* @__PURE__ */ jsx2("span", { className: "rounded-full border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground", children: dirty ? "Unsaved changes" : "Saved" }),
|
|
555
|
+
/* @__PURE__ */ jsx2(
|
|
556
|
+
"button",
|
|
557
|
+
{
|
|
558
|
+
type: "button",
|
|
559
|
+
onClick: onSave,
|
|
560
|
+
disabled: saving || !dirty,
|
|
561
|
+
className: "inline-flex h-7 items-center rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50",
|
|
562
|
+
children: saving ? "Saving\u2026" : "Save"
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
] })
|
|
566
|
+
] }),
|
|
567
|
+
/* @__PURE__ */ jsx2(
|
|
568
|
+
"textarea",
|
|
569
|
+
{
|
|
570
|
+
value: content,
|
|
571
|
+
onChange: (event) => onChange(event.target.value),
|
|
572
|
+
spellCheck: false,
|
|
573
|
+
"aria-label": "Source editor",
|
|
574
|
+
className: "min-h-0 flex-1 resize-none border-0 bg-background p-4 font-mono text-sm leading-6 text-foreground outline-none"
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
] });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export {
|
|
581
|
+
ConfirmDialog,
|
|
582
|
+
VaultPane
|
|
583
|
+
};
|
|
584
|
+
//# sourceMappingURL=chunk-SMVD76VC.js.map
|