flint-agent 1.14.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.
Files changed (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,306 @@
1
+ // Agent slice: status, currentTool, processingCount, pendingAction, toolActivities, thoughts
2
+ import { createLogger } from "../logging/logger.js";
3
+
4
+ const log = createLogger("agent-slice");
5
+
6
+ export const createAgentSlice = (set, get) => ({
7
+ agentStatus: "idle", // idle | thinking | calling-tool | streaming
8
+ currentTool: null,
9
+ // { kind: "call"|"answering"|"tool"|"wait"|"stall", label, attempt } — the
10
+ // current activity, with its own clock in activityStartedAt.
11
+ activity: null,
12
+ activityStartedAt: null,
13
+ // Last output line of the command the activity is about (run_command), so a
14
+ // long foreground command shows progress, not only a clock.
15
+ activityDetail: null,
16
+ activityTokens: 0,
17
+ processingCount: 0,
18
+ pendingAction: null, // null | "restart" | "clear-context"
19
+ pendingConfirmation: null, // { id, toolName, args, resolve }
20
+ pendingPairing: null, // { sessionId, pin, fromAddress, expiresAt }
21
+ toolActivities: [], // [{ id, name, args, result, status: "running"|"done", ts }]
22
+ nextToolActivityId: 1,
23
+ autoMode: null, // null | { iteration, maxIterations, cost, tasksDone, tasksTotal }
24
+ thoughts: [], // collapsed thinking blocks: [{ id, text, ts }]
25
+ showThoughts: false, // toggle via Ctrl+T
26
+
27
+ setAgentStatus(status, tool) {
28
+ log.debug("setAgentStatus", { from: get().agentStatus, to: status, tool: tool || null });
29
+ const update = { agentStatus: status, currentTool: tool || null };
30
+ if (status !== "idle" && !get()._startedAt) update._startedAt = Date.now();
31
+ if (status === "idle") update._startedAt = null;
32
+ set(update);
33
+ },
34
+
35
+ /**
36
+ * What Flint is doing right now, and the clock for THIS thing.
37
+ *
38
+ * The status line used to time the whole turn from `_startedAt`, and called
39
+ * it "elapsed": after fifteen ordinary steps it read `thinking #15 468s`,
40
+ * which is indistinguishable from a hung call. On 2026-09-29 a call that
41
+ * never returned showed exactly that number for ten minutes and there was
42
+ * nothing on screen saying it was waiting rather than working.
43
+ *
44
+ * So every change of activity restarts the clock, and the label says which
45
+ * activity: waiting for the model, running a command, waiting to retry.
46
+ * The number next to it is how long THIS has been going, which is the only
47
+ * number an operator can act on.
48
+ */
49
+ setActivity({ kind, label, attempt, tool, arg } = {}) {
50
+ set({
51
+ activity: { kind: kind || "call", label: label || "", attempt: attempt || null, tool: tool || null, arg: arg || null },
52
+ activityStartedAt: Date.now(),
53
+ activityDetail: null,
54
+ activityTokens: 0,
55
+ });
56
+ },
57
+
58
+ /** Tokens streamed in the current model call, for the activity row. */
59
+ setActivityTokens(n) {
60
+ set({ activityTokens: n || 0 });
61
+ },
62
+
63
+ /** Update the detail without restarting the activity clock. */
64
+ setActivityDetail(text) {
65
+ set({ activityDetail: text || null });
66
+ },
67
+
68
+ clearActivity() {
69
+ set({ activity: null, activityStartedAt: null, activityDetail: null, activityTokens: 0 });
70
+ },
71
+
72
+ incrementQueue() {
73
+ set({ processingCount: get().processingCount + 1 });
74
+ },
75
+
76
+ decrementQueue() {
77
+ set({ processingCount: Math.max(0, get().processingCount - 1) });
78
+ },
79
+
80
+ setPendingAction(action) {
81
+ set({ pendingAction: action });
82
+ },
83
+
84
+ clearPendingAction() {
85
+ set({ pendingAction: null });
86
+ },
87
+
88
+ setPendingConfirmation(conf) {
89
+ set({ pendingConfirmation: conf });
90
+ },
91
+
92
+ clearPendingConfirmation() {
93
+ set({ pendingConfirmation: null });
94
+ },
95
+
96
+ setPendingPairing(pairing) {
97
+ set({ pendingPairing: pairing });
98
+ },
99
+
100
+ clearPendingPairing() {
101
+ set({ pendingPairing: null });
102
+ },
103
+
104
+ addToolActivity({ name, args }) {
105
+ const { toolActivities, nextToolActivityId } = get();
106
+ const entry = { id: nextToolActivityId, name, args, result: "", status: "running", ts: Date.now() };
107
+ const updated = [...toolActivities, entry];
108
+ set({
109
+ toolActivities: updated.length > 30 ? updated.slice(-30) : updated,
110
+ nextToolActivityId: nextToolActivityId + 1,
111
+ });
112
+ return nextToolActivityId;
113
+ },
114
+
115
+ updateToolActivity(id, update) {
116
+ const { toolActivities } = get();
117
+ set({
118
+ toolActivities: toolActivities.map((a) =>
119
+ a.id === id
120
+ ? { ...a, ...update, ...(update.status === "done" ? { endTs: Date.now() } : {}) }
121
+ : a
122
+ ),
123
+ });
124
+ },
125
+
126
+ clearToolActivities() {
127
+ set({ toolActivities: [], nextToolActivityId: 1 });
128
+ },
129
+
130
+ addThought(text) {
131
+ const { thoughts } = get();
132
+ const entry = { id: thoughts.length + 1, text, ts: Date.now() };
133
+ set({ thoughts: [...thoughts, entry] });
134
+ },
135
+
136
+ toggleThoughts() {
137
+ set({ showThoughts: !get().showThoughts });
138
+ },
139
+
140
+ clearThoughts() {
141
+ set({ thoughts: [], showThoughts: false });
142
+ },
143
+
144
+ // -- TaskRegistry: track all running tasks for Escape abort --
145
+ // Order: agent-loop first, then bg-processes (newest→oldest), then child-agents (newest→oldest)
146
+ _taskRegistry: [],
147
+ _nextTaskId: 1,
148
+
149
+ registerTask({ type, label, abort, kill, pid, port }) {
150
+ const { _taskRegistry, _nextTaskId } = get();
151
+ const entry = { id: _nextTaskId, type, label, abort, kill, pid, port, ts: Date.now() };
152
+ set({ _taskRegistry: [..._taskRegistry, entry], _nextTaskId: _nextTaskId + 1 });
153
+ return _nextTaskId;
154
+ },
155
+
156
+ unregisterTask(id) {
157
+ const { _taskRegistry } = get();
158
+ set({ _taskRegistry: _taskRegistry.filter(t => t.id !== id) });
159
+ },
160
+
161
+ // -- Step-scoped abort for the agent loop (Esc) --
162
+ //
163
+ // The whole-loop controller is `signal` in the agent loop, registered above
164
+ // as an "agent-loop" task. Esc must not reach that one. Esc means "stop the
165
+ // step you are on", and until now that was the same call as killing the
166
+ // task: on 2026-09-30 an answer to a question typed at 19:59:52 cost the
167
+ // operator the question (bus flush) and the task (loop abort) in the same
168
+ // instant. The API's /stop keeps the whole loop, deliberately — a harness
169
+ // that calls /stop means it.
170
+ _stepAbort: null,
171
+
172
+ setStepAbort(abort) {
173
+ set({ _stepAbort: abort || null });
174
+ },
175
+
176
+ clearStepAbort() {
177
+ set({ _stepAbort: null });
178
+ },
179
+
180
+ /**
181
+ * Stop the current step of the running agent loop, keeping the task.
182
+ *
183
+ * Returns true only when a step was actually stopped, so the caller can tell
184
+ * "I interrupted something" from "there was nothing to interrupt" and stay
185
+ * quiet about the latter.
186
+ *
187
+ * The loop notices by polling its own step signal at the top of the next
188
+ * iteration. Nothing is called from here into the loop, so a step that
189
+ * ignores its signal simply finishes and the task continues anyway.
190
+ *
191
+ * A controller that has already fired is not a step anybody can stop, so
192
+ * this returns false for it.
193
+ *
194
+ * The check is the honest half of the fix. A spent AbortController's abort()
195
+ * does nothing and does not complain, so before this the second and every
196
+ * later Esc in a turn returned true, and index.js printed
197
+ * "[Step stopped] — the task continues" for a step that was still running:
198
+ * owner 2026-09-30 14:16, session 2026-09-30T19-05-41, eleven such lines
199
+ * stacked on screen, none of them true. The loop now publishes a fresh
200
+ * controller for every step (agent.js, newStep), so the spent state is only
201
+ * ever the narrow window between one step ending and the next starting —
202
+ * which is exactly the window in which the answer really is "nothing was
203
+ * stopped".
204
+ */
205
+ abortStep() {
206
+ const s = get();
207
+ const loopIsRunning = s._taskRegistry.some(t => t.type === "agent-loop");
208
+ if (!loopIsRunning || !s._stepAbort) return false;
209
+ if (s._stepAbort.signal?.aborted) return false;
210
+ s._stepAbort.abort();
211
+ return true;
212
+ },
213
+
214
+ /**
215
+ * What one Escape press means: end the agent task, keep the queue.
216
+ *
217
+ * Returns the task it stopped, or null when there was nothing to stop — the
218
+ * caller prints nothing in that case.
219
+ *
220
+ * It was the step, and that was wrong for the operator who pressed it.
221
+ * Owner, 2026-09-30: one question, then ten presses of Esc against the
222
+ * running task. Ten "[Step stopped]" lines and the task carried on, because
223
+ * the loop restarts a cancelled step itself (agent.js newStep) — so each
224
+ * press stopped a step that the next instant began again, and the key
225
+ * reported an interruption while producing none. Ten presses were one
226
+ * message and one intent: stop.
227
+ *
228
+ * So the step signal is fired first (a model call in flight ends at once
229
+ * rather than at the next loop boundary, which is where a hung call lives)
230
+ * and then the whole-loop signal ends the task itself.
231
+ *
232
+ * The queue is NOT touched, and that half is not optional. flush() fails
233
+ * every pending bus message as "flushed": session 2026-09-30T00-37-42 lost
234
+ * the question typed at 19:59:52 and the task it was asking about 19 ms
235
+ * apart, from one press. Esc keeps the queue; `stop` is the word that
236
+ * discards it, and the API's /stop still means the whole task.
237
+ */
238
+ escAbort() {
239
+ const s = get();
240
+ const loopIsRunning = s._taskRegistry.some(t => t.type === "agent-loop");
241
+ if (loopIsRunning) {
242
+ // Both signals, in that order: the step ends the call, the loop ends the
243
+ // task. abortStep() is the one that must not be skipped — it is what
244
+ // makes the key answer on a hung provider instead of at the next boundary.
245
+ s.abortStep();
246
+ return get().abortNext();
247
+ }
248
+ // No agent task: Esc is the emergency brake for background work, newest
249
+ // first, one per press (owner, 2026-10-01: "if something was started by
250
+ // accident, I must be able to stop it fast"). abortNext() is that order:
251
+ // background processes newest first, then child agents.
252
+ return get().abortNext();
253
+ },
254
+
255
+ /**
256
+ * Abort next task in priority order:
257
+ * 1. agent-loop (if active)
258
+ * 2. bg-process (newest first)
259
+ * 3. child-agent (newest first)
260
+ * Returns { type, label } of aborted task, or null if nothing to abort.
261
+ */
262
+ abortNext() {
263
+ const { _taskRegistry } = get();
264
+ if (!_taskRegistry.length) return null;
265
+
266
+ // Priority 1: agent-loop
267
+ const agentLoop = _taskRegistry.find(t => t.type === "agent-loop");
268
+ if (agentLoop) {
269
+ if (agentLoop.abort) agentLoop.abort.abort();
270
+ set({ _taskRegistry: _taskRegistry.filter(t => t.id !== agentLoop.id) });
271
+ return { type: agentLoop.type, label: agentLoop.label };
272
+ }
273
+
274
+ // Priority 2: bg-process (newest first)
275
+ const bgProcesses = _taskRegistry.filter(t => t.type === "bg-process").sort((a, b) => b.ts - a.ts);
276
+ if (bgProcesses.length) {
277
+ const target = bgProcesses[0];
278
+ if (target.kill) target.kill();
279
+ set({ _taskRegistry: _taskRegistry.filter(t => t.id !== target.id) });
280
+ return { type: target.type, label: target.label };
281
+ }
282
+
283
+ // Priority 3: child-agent (newest first)
284
+ const agents = _taskRegistry.filter(t => t.type === "child-agent").sort((a, b) => b.ts - a.ts);
285
+ if (agents.length) {
286
+ const target = agents[0];
287
+ if (target.kill) target.kill();
288
+ set({ _taskRegistry: _taskRegistry.filter(t => t.id !== target.id) });
289
+ return { type: target.type, label: target.label };
290
+ }
291
+
292
+ return null;
293
+ },
294
+
295
+ abortAll() {
296
+ const { _taskRegistry } = get();
297
+ const results = [];
298
+ for (const task of _taskRegistry) {
299
+ if (task.abort) task.abort.abort();
300
+ if (task.kill) task.kill();
301
+ results.push({ type: task.type, label: task.label });
302
+ }
303
+ set({ _taskRegistry: [] });
304
+ return results;
305
+ },
306
+ });
@@ -0,0 +1,73 @@
1
+ // Dataset slice — named datasets with pagination
2
+ // Stores structured data from tool results, survives context compression
3
+
4
+ const MAX_DATASETS = 10;
5
+
6
+ export function createDatasetSlice(set, get) {
7
+ return {
8
+ datasets: {},
9
+ _datasetSeq: 1,
10
+
11
+ addDataset({ label, columns, rows, pageSize = 10, source }) {
12
+ const id = `ds_${get()._datasetSeq}`;
13
+ const datasets = { ...get().datasets };
14
+
15
+ // Evict oldest if at capacity
16
+ const keys = Object.keys(datasets);
17
+ if (keys.length >= MAX_DATASETS) {
18
+ let oldest = keys[0];
19
+ for (const k of keys) {
20
+ if (datasets[k].createdAt < datasets[oldest].createdAt) oldest = k;
21
+ }
22
+ delete datasets[oldest];
23
+ }
24
+
25
+ datasets[id] = {
26
+ id,
27
+ label: label || "data",
28
+ columns,
29
+ rows,
30
+ page: 1,
31
+ pageSize,
32
+ source: source || "unknown",
33
+ createdAt: Date.now(),
34
+ };
35
+
36
+ set({ datasets, _datasetSeq: get()._datasetSeq + 1 });
37
+ return id;
38
+ },
39
+
40
+ setDatasetPage(id, page) {
41
+ const ds = get().datasets[id];
42
+ if (!ds) return;
43
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
44
+ const clamped = Math.max(1, Math.min(page, totalPages));
45
+ set({
46
+ datasets: {
47
+ ...get().datasets,
48
+ [id]: { ...ds, page: clamped },
49
+ },
50
+ });
51
+ },
52
+
53
+ getDatasetPage(id) {
54
+ const ds = get().datasets[id];
55
+ if (!ds) return null;
56
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
57
+ const start = (ds.page - 1) * ds.pageSize;
58
+ return {
59
+ id: ds.id,
60
+ label: ds.label,
61
+ columns: ds.columns,
62
+ rows: ds.rows.slice(start, start + ds.pageSize),
63
+ page: ds.page,
64
+ totalPages,
65
+ totalRows: ds.rows.length,
66
+ };
67
+ },
68
+
69
+ clearDatasets() {
70
+ set({ datasets: {} });
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,22 @@
1
+ import { createStore } from "zustand/vanilla";
2
+ import { createSessionSlice } from "./session-slice.js";
3
+ import { createAgentSlice } from "./agent-slice.js";
4
+ import { createUiSlice } from "./ui-slice.js";
5
+ import { createProcessSlice } from "./process-slice.js";
6
+ import { createDatasetSlice } from "./dataset-slice.js";
7
+
8
+ export const store = createStore((...args) => ({
9
+ ...createSessionSlice(...args),
10
+ ...createAgentSlice(...args),
11
+ ...createUiSlice(...args),
12
+ ...createProcessSlice(...args),
13
+ ...createDatasetSlice(...args),
14
+ }));
15
+
16
+ // Convenience: useStore hook for React components
17
+ // Usage: const lines = useStore(s => s.lines)
18
+ export function useStore(selector) {
19
+ // This is implemented in the React layer — see components/App.js
20
+ // For vanilla (non-React) access, use store.getState() and store.subscribe()
21
+ throw new Error("useStore must be used within React — use store.getState() outside React");
22
+ }
@@ -0,0 +1,135 @@
1
+ // Process slice: background processes tracking
2
+ import { spawn } from "node:child_process";
3
+
4
+ export const createProcessSlice = (set, get) => ({
5
+ processes: [], // [{ id, cmd, status, startTime, pid, output[], exitCode }]
6
+ nextProcessId: 1,
7
+
8
+ addProcess({ cmd, command, pid }) {
9
+ const { processes, nextProcessId } = get();
10
+ const proc = {
11
+ id: nextProcessId,
12
+ cmd,
13
+ command: command || cmd, // what the screen shows; cmd may be a label
14
+ pid: pid || null,
15
+ status: "running", // "running" | "done" | "failed" | "killed"
16
+ startTime: Date.now(),
17
+ output: [], // last N lines of combined stdout+stderr
18
+ exitCode: null,
19
+ };
20
+ set({
21
+ processes: [...processes, proc],
22
+ nextProcessId: nextProcessId + 1,
23
+ });
24
+ return nextProcessId;
25
+ },
26
+
27
+ appendProcessOutput(id, line) {
28
+ const { processes } = get();
29
+ set({
30
+ processes: processes.map((p) => {
31
+ if (p.id !== id) return p;
32
+ // Dedup: collapse consecutive identical lines into count
33
+ const last = p.output[p.output.length - 1];
34
+ if (last && last.replace(/\s*\(x\d+\)$/, "") === line) {
35
+ const match = last.match(/\(x(\d+)\)$/);
36
+ const count = match ? parseInt(match[1], 10) + 1 : 2;
37
+ const deduped = [...p.output];
38
+ deduped[deduped.length - 1] = `${line} (x${count})`;
39
+ return { ...p, output: deduped };
40
+ }
41
+ const output = [...p.output, line];
42
+ // Keep last 50 lines
43
+ return { ...p, output: output.length > 50 ? output.slice(-50) : output };
44
+ }),
45
+ });
46
+ },
47
+
48
+ finishProcess(id, exitCode, status) {
49
+ const { processes } = get();
50
+ set({
51
+ processes: processes.map((p) => {
52
+ if (p.id !== id) return p;
53
+ // A process stopped with /kill exits non-zero (taskkill gives 1). It was
54
+ // killed, not failed: keep that, or the agent reads it as a crash
55
+ // (owner demo, 2026-10-01).
56
+ if (p.status === "killed" && !status) return { ...p, exitCode };
57
+ return { ...p, exitCode, status: status || (exitCode === 0 ? "done" : "failed") };
58
+ }),
59
+ });
60
+ },
61
+
62
+ killProcess(id) {
63
+ const { processes } = get();
64
+ const proc = processes.find((p) => p.id === id);
65
+ if (proc && proc.pid && proc.status === "running") {
66
+ try {
67
+ if (process.platform === "win32") {
68
+ spawn("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
69
+ } else {
70
+ process.kill(proc.pid, "SIGTERM");
71
+ }
72
+ } catch {}
73
+ set({
74
+ processes: processes.map((p) =>
75
+ p.id === id ? { ...p, status: "killed" } : p
76
+ ),
77
+ });
78
+ return true;
79
+ }
80
+ return false;
81
+ },
82
+
83
+ killAllRunning() {
84
+ const { processes } = get();
85
+ const running = processes.filter((p) => p.status === "running" && p.pid);
86
+ for (const proc of running) {
87
+ try {
88
+ if (process.platform === "win32") {
89
+ spawn("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
90
+ } else {
91
+ process.kill(proc.pid, "SIGTERM");
92
+ }
93
+ } catch {}
94
+ }
95
+ set({
96
+ processes: processes.map((p) =>
97
+ p.status === "running" ? { ...p, status: "killed" } : p
98
+ ),
99
+ });
100
+ return running.length;
101
+ },
102
+
103
+ // Internal timers / scheduled jobs
104
+ timers: [], // [{ id, name, intervalMs, nextRun, lastRun, status }]
105
+
106
+ registerTimer({ id, name, intervalMs }) {
107
+ const { timers } = get();
108
+ const existing = timers.find((t) => t.id === id);
109
+ if (existing) return;
110
+ set({
111
+ timers: [...timers, {
112
+ id,
113
+ name,
114
+ intervalMs,
115
+ nextRun: Date.now() + intervalMs,
116
+ lastRun: null,
117
+ status: "active",
118
+ }],
119
+ });
120
+ },
121
+
122
+ updateTimerRun(id) {
123
+ const { timers } = get();
124
+ set({
125
+ timers: timers.map((t) =>
126
+ t.id === id ? { ...t, lastRun: Date.now(), nextRun: Date.now() + t.intervalMs } : t
127
+ ),
128
+ });
129
+ },
130
+
131
+ removeTimer(id) {
132
+ const { timers } = get();
133
+ set({ timers: timers.filter((t) => t.id !== id) });
134
+ },
135
+ });