@paradow/ledge 0.0.0-stage → 0.3.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.
@@ -0,0 +1,216 @@
1
+ import {
2
+ cleanTopic,
3
+ sessionTitle
4
+ } from "./chunk-6GMDFIGB.js";
5
+
6
+ // src/daemon/state.ts
7
+ var HOOK_PRIORITY_MS = 2e3;
8
+ var hooksReportPrompts = (s) => s.tool === "claude";
9
+ var QUESTION_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "request_user_input"]);
10
+ var DIFF_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "apply_patch"]);
11
+ function pendingFor(ev) {
12
+ if (ev.name === "PermissionRequest") {
13
+ const diff = !!ev.tool && DIFF_TOOLS.has(ev.tool);
14
+ return { kind: diff ? "diff" : "permission", text: ev.text ?? "", tool: ev.tool, source: "hook" };
15
+ }
16
+ if (ev.name === "PreToolUse" && ev.tool && QUESTION_TOOLS.has(ev.tool)) {
17
+ return { kind: "question", text: ev.text ?? "", options: ev.options, source: "hook", ...ev.tool !== "AskUserQuestion" ? { tool: ev.tool } : {} };
18
+ }
19
+ if (ev.name === "PreToolUse" && ev.tool === "ExitPlanMode") {
20
+ return { kind: "plan", text: ev.text ?? "", source: "hook" };
21
+ }
22
+ if (ev.name === "Notification" && ev.kind === "permission") {
23
+ return { kind: "permission", text: ev.text ?? "", source: "hook" };
24
+ }
25
+ if (ev.name === "PanePrompt") {
26
+ return { kind: ev.kind ?? "permission", text: ev.text ?? "", options: ev.options, source: "pane" };
27
+ }
28
+ return void 0;
29
+ }
30
+ function mergePending(current, incoming) {
31
+ if (!current) return incoming;
32
+ if (current.source === "hook" && incoming.source === "pane") return current;
33
+ if (incoming.source === "hook" && current.source === "pane") return incoming;
34
+ if (current.helperId !== incoming.helperId) return current;
35
+ if (current.source === "hook" && (current.kind === "plan" || current.kind === "question") && incoming.source === "hook") return current;
36
+ const kind = incoming.kind;
37
+ return { ...current, ...incoming, kind, text: incoming.text || current.text, tool: incoming.tool ?? current.tool, options: incoming.options ?? current.options };
38
+ }
39
+ function pendingTool(p) {
40
+ if (p.kind === "question") return p.tool ?? "AskUserQuestion";
41
+ if (p.kind === "plan") return "ExitPlanMode";
42
+ return p.tool;
43
+ }
44
+ function clearedByToolRun(p, tool) {
45
+ const t = pendingTool(p);
46
+ return p.source === "pane" || t === void 0 || t === tool;
47
+ }
48
+ function setHelper(s, id, patch, kind = "subagent") {
49
+ const i = s.helpers.findIndex((h) => h.id === id);
50
+ if (i < 0) return [...s.helpers, { id, kind, task: "", state: "working", ...patch }];
51
+ const helpers = [...s.helpers];
52
+ helpers[i] = { ...helpers[i], ...patch };
53
+ return helpers;
54
+ }
55
+ function settled(state) {
56
+ return state === "waiting_permission" || state === "waiting_input" ? "working" : state;
57
+ }
58
+ function restored(s) {
59
+ return s.pending?.parentState ?? settled(s.state);
60
+ }
61
+ function underHelper(s, parentState, at, extra = {}) {
62
+ return { ...s, ...extra, pending: { ...s.pending, parentState }, lastHookAt: at, lastActivityAt: at };
63
+ }
64
+ function adoptCursorTask(s, id) {
65
+ if (s.tool !== "cursor" || id.startsWith("task:") || s.helpers.some((h) => h.id === id)) return s;
66
+ const i = s.helpers.findIndex((h) => h.id.startsWith("task:") && h.state === "working");
67
+ if (i < 0) return s;
68
+ const helpers = [...s.helpers];
69
+ helpers[i] = { ...helpers[i], id };
70
+ return { ...s, helpers };
71
+ }
72
+ var untracked = (s, id) => s.tool === "claude" && !s.helpers.some((h) => h.id === id);
73
+ function reduceHelper(s, ev) {
74
+ s = adoptCursorTask(s, ev.helperId);
75
+ const at = ev.at;
76
+ const id = ev.helperId;
77
+ const own = s.pending?.helperId === id;
78
+ const named = ev.task && !s.helpers.find((h) => h.id === id)?.task ? { task: ev.task } : {};
79
+ const track = (patch, kind) => untracked(s, id) ? s.helpers : setHelper(s, id, { ...patch, ...named }, kind);
80
+ switch (ev.name) {
81
+ case "SubagentStart": {
82
+ const known = s.helpers.find((h) => h.id === id);
83
+ const patch = { state: "working", kind: ev.tool ?? known?.kind ?? "subagent", task: ev.text || known?.task || ev.task || "" };
84
+ return { ...s, helpers: setHelper(s, id, patch), lastHookAt: at, lastActivityAt: at };
85
+ }
86
+ case "SubagentStop": {
87
+ const helpers = track({ state: "exited" }, ev.tool ?? "subagent");
88
+ if (own) return { ...s, helpers, pending: void 0, state: restored(s), lastHookAt: at, lastActivityAt: at };
89
+ return { ...s, helpers, lastHookAt: at, lastActivityAt: at };
90
+ }
91
+ case "PermissionRequest": {
92
+ const helpers = track({ state: "waiting_permission" });
93
+ if (s.pending && s.pending.source === "hook") return { ...s, helpers, lastHookAt: at, lastActivityAt: at };
94
+ const pending = { ...pendingFor(ev), helperId: id, parentState: settled(s.state) };
95
+ return { ...s, helpers, state: "waiting_permission", pending, lastHookAt: at, lastActivityAt: at };
96
+ }
97
+ case "PreToolUse":
98
+ case "PostToolUse":
99
+ case "PostToolUseFailure": {
100
+ const helper = s.helpers.find((h) => h.id === id);
101
+ const busy = ev.name === "PreToolUse" && helper?.state === "waiting_permission";
102
+ const helpers = busy ? s.helpers : track({ state: ev.interrupt ? "idle" : "working" });
103
+ if (ev.name !== "PreToolUse" && own && (ev.interrupt || clearedByToolRun(s.pending, ev.tool))) {
104
+ return { ...s, helpers, pending: void 0, state: restored(s), lastHookAt: at, lastActivityAt: at };
105
+ }
106
+ return { ...s, helpers, lastHookAt: at, lastActivityAt: at };
107
+ }
108
+ default:
109
+ return { ...s, lastHookAt: at, lastActivityAt: at };
110
+ }
111
+ }
112
+ function withoutReply(s) {
113
+ const { lastReply: _gone, ...rest } = s;
114
+ return rest;
115
+ }
116
+ function samePending(a, b) {
117
+ return a.kind === b.kind && a.text === b.text && a.tool === b.tool && a.source === b.source && JSON.stringify(a.options) === JSON.stringify(b.options);
118
+ }
119
+ function endCursorHelpers(s, ev) {
120
+ if (s.tool !== "cursor" || ev.source !== "hook" || ev.helperId || !s.helpers.some((h) => h.state === "working")) return s;
121
+ const moves = ev.name === "PostToolUse" || ev.name === "PostToolUseFailure" || ev.name === "Stop" || ev.name === "StopFailure" || ev.name === "UserPromptSubmit" || ev.name === "PreToolUse";
122
+ return moves ? { ...s, helpers: s.helpers.map((h) => h.state === "working" ? { ...h, state: "exited" } : h) } : s;
123
+ }
124
+ function withTopic(s, text, source) {
125
+ const topic = cleanTopic(text) || void 0;
126
+ if (topic === s.topic && (topic ? source : void 0) === s.topicSource) return s;
127
+ const { topic: _t, topicSource: _ts, ...rest } = s;
128
+ const next = topic ? { ...rest, topic, topicSource: source } : rest;
129
+ return { ...next, title: sessionTitle(next) };
130
+ }
131
+ function reduce(s, ev) {
132
+ if (s.state === "exited") return s;
133
+ s = endCursorHelpers(s, ev);
134
+ const at = ev.at;
135
+ const lastHookAt = s.lastHookAt;
136
+ if (ev.name === "SubagentStart" || ev.name === "SubagentStop") return ev.helperId ? reduceHelper(s, { ...ev, helperId: ev.helperId }) : s;
137
+ if (ev.helperId && ev.source === "hook" && ev.name !== "SessionEnd") return reduceHelper(s, { ...ev, helperId: ev.helperId });
138
+ switch (ev.name) {
139
+ case "SessionEnd":
140
+ return { ...s, state: "exited", pending: void 0, exitCode: ev.exitCode ?? null, exitedAt: s.exitedAt ?? at, lastActivityAt: at };
141
+ case "PaneGone":
142
+ return { ...s, state: "exited", pending: void 0, exitCode: null, exitedAt: s.exitedAt ?? at, lastActivityAt: at };
143
+ case "PaneChanged":
144
+ return s.lastActivityAt === at ? s : { ...s, lastActivityAt: at };
145
+ case "PaneTitle":
146
+ return s.topicSource === "user" ? s : withTopic(s, ev.text ?? "", "pane");
147
+ case "Renamed":
148
+ return withTopic(s, ev.text ?? "", ev.text?.trim() ? "user" : void 0);
149
+ case "PaneIdle": {
150
+ if (s.state === "idle" || s.state === "error" || s.pending?.helperId) return s;
151
+ if (lastHookAt && Date.parse(at) - Date.parse(lastHookAt) < HOOK_PRIORITY_MS) return s;
152
+ return { ...s, state: "idle", pending: void 0, lastActivityAt: at };
153
+ }
154
+ case "InjectFailed":
155
+ return { ...s, state: "error", lastError: ev.text ?? "injection not confirmed", lastActivityAt: at };
156
+ case "Decided": {
157
+ const helperId = s.pending?.helperId;
158
+ if (helperId) {
159
+ const state = restored(s);
160
+ const helpers = s.helpers.some((h) => h.id === helperId) ? setHelper(s, helperId, { state: "working" }) : s.helpers;
161
+ return { ...s, state, helpers, pending: void 0, lastError: state === "error" ? s.lastError : void 0, lastActivityAt: at };
162
+ }
163
+ return { ...s, state: "working", pending: void 0, lastError: void 0, lastHookAt: at, lastActivityAt: at };
164
+ }
165
+ case "Stop": {
166
+ const lastReply = ev.text ? { text: ev.text, at } : s.lastReply;
167
+ if (s.pending?.helperId) return underHelper(s, "idle", at, { lastReply });
168
+ return { ...s, state: "idle", pending: void 0, lastReply, lastHookAt: at, lastActivityAt: at };
169
+ }
170
+ case "StopFailure": {
171
+ if (s.state === "idle") return s;
172
+ const lastError = ev.text || "the turn stopped on an error";
173
+ if (s.pending?.helperId) return underHelper(s, "error", at, { lastError });
174
+ return { ...s, state: "error", pending: void 0, lastError, lastHookAt: at, lastActivityAt: at };
175
+ }
176
+ case "PostToolUse":
177
+ case "PostToolUseFailure": {
178
+ const rest = ev.interrupt ? "idle" : "working";
179
+ if (s.pending?.helperId) return underHelper(s, rest, at);
180
+ if (s.pending && !ev.interrupt && !clearedByToolRun(s.pending, ev.tool)) return { ...s, lastHookAt: at, lastActivityAt: at };
181
+ return { ...s, state: rest, pending: void 0, lastHookAt: at, lastActivityAt: at };
182
+ }
183
+ case "PanePrompt": {
184
+ if (s.pending?.source === "hook") return s;
185
+ if (hooksReportPrompts(s) && lastHookAt && Date.parse(at) - Date.parse(lastHookAt) < HOOK_PRIORITY_MS) return s;
186
+ const pending = mergePending(s.pending, pendingFor(ev));
187
+ if (s.pending && samePending(s.pending, pending)) return s;
188
+ const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
189
+ return { ...s, state, pending, lastActivityAt: at };
190
+ }
191
+ case "SessionStart":
192
+ case "UserPromptSubmit":
193
+ case "PreToolUse":
194
+ case "PermissionRequest":
195
+ case "Notification": {
196
+ if (ev.name === "UserPromptSubmit" && ev.text && !s.topic) s = withTopic(s, ev.text, "prompt");
197
+ const incoming = pendingFor(ev);
198
+ if (incoming) {
199
+ if (ev.name === "Notification" && s.pending?.source === "hook") return { ...s, lastHookAt: at, lastActivityAt: at };
200
+ const pending = mergePending(s.pending, incoming);
201
+ const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
202
+ return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
203
+ }
204
+ if (ev.name === "Notification" || ev.name === "SessionStart") return { ...s, lastHookAt: at, lastActivityAt: at };
205
+ const prompted = ev.name === "UserPromptSubmit" && s.lastReply ? withoutReply(s) : s;
206
+ if (prompted.pending?.helperId) return underHelper(prompted, "working", at);
207
+ if (prompted === s && s.state === "working" && s.lastActivityAt === at && !s.pending) return s;
208
+ return { ...prompted, state: "working", pending: void 0, lastHookAt: at, lastActivityAt: at };
209
+ }
210
+ }
211
+ }
212
+
213
+ export {
214
+ QUESTION_TOOLS,
215
+ reduce
216
+ };