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.
- package/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- 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
|
+
});
|