hydra-os-cli 0.1.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/README.md +274 -0
- package/dist/app.d.ts +12 -0
- package/dist/app.js +127 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +177 -0
- package/dist/clients/api.d.ts +115 -0
- package/dist/clients/api.js +123 -0
- package/dist/clients/qdrant.d.ts +39 -0
- package/dist/clients/qdrant.js +34 -0
- package/dist/clients/temporal.d.ts +37 -0
- package/dist/clients/temporal.js +32 -0
- package/dist/commands/agent.d.ts +4 -0
- package/dist/commands/agent.js +103 -0
- package/dist/commands/artifact.d.ts +4 -0
- package/dist/commands/artifact.js +42 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +80 -0
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.js +20 -0
- package/dist/commands/memory.d.ts +4 -0
- package/dist/commands/memory.js +24 -0
- package/dist/commands/registry.d.ts +23 -0
- package/dist/commands/registry.js +23 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.js +15 -0
- package/dist/commands/workflow.d.ts +5 -0
- package/dist/commands/workflow.js +301 -0
- package/dist/config.d.ts +152 -0
- package/dist/config.js +91 -0
- package/dist/screens/help.d.ts +5 -0
- package/dist/screens/help.js +14 -0
- package/dist/screens/main.d.ts +5 -0
- package/dist/screens/main.js +5 -0
- package/dist/screens/workflow-detail.d.ts +9 -0
- package/dist/screens/workflow-detail.js +11 -0
- package/dist/screens/workflow-list.d.ts +5 -0
- package/dist/screens/workflow-list.js +10 -0
- package/dist/sse.d.ts +16 -0
- package/dist/sse.js +197 -0
- package/dist/store.d.ts +100 -0
- package/dist/store.js +64 -0
- package/dist/widgets/agent-panel.d.ts +15 -0
- package/dist/widgets/agent-panel.js +23 -0
- package/dist/widgets/approval-modal.d.ts +16 -0
- package/dist/widgets/approval-modal.js +24 -0
- package/dist/widgets/artifact-tree.d.ts +14 -0
- package/dist/widgets/artifact-tree.js +9 -0
- package/dist/widgets/chat-panel.d.ts +10 -0
- package/dist/widgets/chat-panel.js +29 -0
- package/dist/widgets/header.d.ts +11 -0
- package/dist/widgets/header.js +14 -0
- package/dist/widgets/health-check.d.ts +15 -0
- package/dist/widgets/health-check.js +19 -0
- package/dist/widgets/input-bar.d.ts +9 -0
- package/dist/widgets/input-bar.js +37 -0
- package/dist/widgets/memory-panel.d.ts +11 -0
- package/dist/widgets/memory-panel.js +9 -0
- package/dist/widgets/status-bar.d.ts +13 -0
- package/dist/widgets/status-bar.js +24 -0
- package/dist/widgets/timeline.d.ts +26 -0
- package/dist/widgets/timeline.js +19 -0
- package/package.json +64 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading and validation for the Hydra TUI.
|
|
3
|
+
*
|
|
4
|
+
* Config file: ~/.hydra/config.yaml
|
|
5
|
+
* Overridable via environment variables.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
export const TuiConfigSchema = z.object({
|
|
13
|
+
temporal: z.object({
|
|
14
|
+
address: z.string().default("localhost:7233"),
|
|
15
|
+
namespace: z.string().default("default"),
|
|
16
|
+
}).default({}),
|
|
17
|
+
api: z.object({
|
|
18
|
+
url: z.string().default("http://localhost:7070"),
|
|
19
|
+
}).default({}),
|
|
20
|
+
qdrant: z.object({
|
|
21
|
+
url: z.string().default("http://localhost:6333"),
|
|
22
|
+
}).default({}),
|
|
23
|
+
tui: z.object({
|
|
24
|
+
theme: z.enum(["dark", "light", "solarized", "monokai", "hydra"]).default("dark"),
|
|
25
|
+
layout: z.enum(["split", "full"]).default("split"),
|
|
26
|
+
rightPanel: z.boolean().default(true),
|
|
27
|
+
vimMode: z.boolean().default(false),
|
|
28
|
+
syntaxHighlighting: z.boolean().default(true),
|
|
29
|
+
maxHistory: z.number().default(1000),
|
|
30
|
+
spinnerStyle: z.enum(["dots", "line", "arc", "bounce"]).default("dots"),
|
|
31
|
+
}).default({}),
|
|
32
|
+
workflow: z.object({
|
|
33
|
+
defaultModel: z.string().default("claude-sonnet-4-5-20250929"),
|
|
34
|
+
autoApproveReads: z.boolean().default(true),
|
|
35
|
+
qualityThreshold: z.number().default(28),
|
|
36
|
+
costWarning: z.number().default(5.0),
|
|
37
|
+
}).default({}),
|
|
38
|
+
notifications: z.object({
|
|
39
|
+
sound: z.boolean().default(true),
|
|
40
|
+
desktop: z.boolean().default(true),
|
|
41
|
+
idleAlert: z.number().default(300),
|
|
42
|
+
}).default({}),
|
|
43
|
+
});
|
|
44
|
+
function deepMerge(target, source) {
|
|
45
|
+
const result = { ...target };
|
|
46
|
+
for (const key of Object.keys(source)) {
|
|
47
|
+
const sv = source[key];
|
|
48
|
+
const tv = result[key];
|
|
49
|
+
if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
|
|
50
|
+
result[key] = deepMerge(tv, sv);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result[key] = sv;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function loadYamlConfig() {
|
|
59
|
+
try {
|
|
60
|
+
const configPath = join(homedir(), ".hydra", "config.yaml");
|
|
61
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
62
|
+
const parsed = parseYaml(raw);
|
|
63
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// No config file or parse error — use defaults
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function getEnvOverrides() {
|
|
71
|
+
const overrides = {};
|
|
72
|
+
if (process.env.TEMPORAL_ADDRESS) {
|
|
73
|
+
overrides.temporal = { address: process.env.TEMPORAL_ADDRESS };
|
|
74
|
+
}
|
|
75
|
+
if (process.env.HYDRA_API_URL) {
|
|
76
|
+
overrides.api = { url: process.env.HYDRA_API_URL };
|
|
77
|
+
}
|
|
78
|
+
if (process.env.QDRANT_URL) {
|
|
79
|
+
overrides.qdrant = { url: process.env.QDRANT_URL };
|
|
80
|
+
}
|
|
81
|
+
if (process.env.HYDRA_THEME) {
|
|
82
|
+
overrides.tui = { theme: process.env.HYDRA_THEME };
|
|
83
|
+
}
|
|
84
|
+
return overrides;
|
|
85
|
+
}
|
|
86
|
+
export function loadConfig() {
|
|
87
|
+
const fileConfig = loadYamlConfig();
|
|
88
|
+
const envOverrides = getEnvOverrides();
|
|
89
|
+
const merged = deepMerge(fileConfig, envOverrides);
|
|
90
|
+
return TuiConfigSchema.parse(merged);
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Help screen.
|
|
4
|
+
* Displays all available commands grouped by category.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { getAllCommands } from "../commands/index.js";
|
|
8
|
+
export function HelpScreen() {
|
|
9
|
+
const commands = getAllCommands();
|
|
10
|
+
const categories = [...new Set(commands.map((c) => c.category))];
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Hydra TUI Commands" }), _jsx(Text, {}), categories.map((cat) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: cat.toUpperCase() }), commands
|
|
12
|
+
.filter((c) => c.category === cat)
|
|
13
|
+
.map((cmd) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", children: `/${cmd.name}`.padEnd(16) }), _jsx(Text, { children: cmd.description })] }, cmd.name)))] }, cat))), _jsx(Text, {}), _jsx(Text, { dimColor: true, children: "Keyboard: Ctrl+P toggle panels | Ctrl+D exit | F1 help | F5 refresh" })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow detail / timeline view screen.
|
|
3
|
+
* Shows full workflow timeline, agent outputs, quality scores, artifacts.
|
|
4
|
+
*/
|
|
5
|
+
interface WorkflowDetailScreenProps {
|
|
6
|
+
workflowId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function WorkflowDetailScreen({ workflowId }: WorkflowDetailScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Workflow detail / timeline view screen.
|
|
4
|
+
* Shows full workflow timeline, agent outputs, quality scores, artifacts.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { Timeline } from "../widgets/timeline.js";
|
|
8
|
+
export function WorkflowDetailScreen({ workflowId }) {
|
|
9
|
+
// TODO: fetch workflow data from API/Temporal
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, children: ["Workflow Detail: ", workflowId] }), _jsx(Timeline, { workflowId: workflowId, workflowStatus: "LOADING", startedAgo: "...", totalCost: "...", steps: [] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Workflow list view screen.
|
|
4
|
+
* Shows all workflows with status, phase, cost, elapsed time.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
export function WorkflowListScreen() {
|
|
8
|
+
// TODO: fetch workflows from API
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, underline: true, children: "Workflows" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No workflows found. Use /start to create one." }) })] }));
|
|
10
|
+
}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) streaming for the Hydra TUI.
|
|
3
|
+
* Fetch-based parser since Node 18+ has no native EventSource.
|
|
4
|
+
*/
|
|
5
|
+
import type { HydraStore } from "./store.js";
|
|
6
|
+
export interface SseEvent {
|
|
7
|
+
event: string;
|
|
8
|
+
data: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to a workflow's SSE stream and push updates to the store.
|
|
13
|
+
* Reconnects with exponential backoff on connection failure (max 5 retries).
|
|
14
|
+
* Returns an AbortController for cleanup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function subscribeToWorkflow(apiUrl: string, workflowId: string, store: HydraStore): AbortController;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) streaming for the Hydra TUI.
|
|
3
|
+
* Fetch-based parser since Node 18+ has no native EventSource.
|
|
4
|
+
*/
|
|
5
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
6
|
+
const BASE_RECONNECT_DELAY_MS = 1000;
|
|
7
|
+
function parseSseLine(line, state) {
|
|
8
|
+
if (line.startsWith(":"))
|
|
9
|
+
return; // Comment / heartbeat
|
|
10
|
+
const colonIdx = line.indexOf(":");
|
|
11
|
+
// Per SSE spec: line with no colon uses entire line as field name with empty value
|
|
12
|
+
const field = colonIdx === -1 ? line : line.slice(0, colonIdx);
|
|
13
|
+
const value = colonIdx === -1 ? "" : line.slice(colonIdx + 1).trimStart();
|
|
14
|
+
switch (field) {
|
|
15
|
+
case "event":
|
|
16
|
+
state.event = value;
|
|
17
|
+
break;
|
|
18
|
+
case "data":
|
|
19
|
+
state.data += (state.data ? "\n" : "") + value;
|
|
20
|
+
break;
|
|
21
|
+
case "id":
|
|
22
|
+
state.id = value;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Async generator that yields parsed SSE events from a fetch stream.
|
|
28
|
+
*/
|
|
29
|
+
async function* sseStream(url, signal) {
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
headers: { Accept: "text/event-stream" },
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
const reader = res.body?.getReader();
|
|
38
|
+
if (!reader)
|
|
39
|
+
throw new Error("No response body");
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = "";
|
|
42
|
+
const state = { event: "", data: "", id: undefined };
|
|
43
|
+
function resetState() {
|
|
44
|
+
state.event = "";
|
|
45
|
+
state.data = "";
|
|
46
|
+
state.id = undefined;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
buffer += decoder.decode(value, { stream: true });
|
|
54
|
+
const lines = buffer.split("\n");
|
|
55
|
+
// Keep the last incomplete line in buffer
|
|
56
|
+
buffer = lines.pop() ?? "";
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line === "") {
|
|
59
|
+
// Empty line = event boundary
|
|
60
|
+
if (state.data) {
|
|
61
|
+
yield {
|
|
62
|
+
event: state.event || "message",
|
|
63
|
+
data: state.data.trimEnd(),
|
|
64
|
+
id: state.id,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
resetState();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
parseSseLine(line, state);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Flush any remaining data in buffer after stream ends
|
|
74
|
+
if (buffer.trim()) {
|
|
75
|
+
parseSseLine(buffer, state);
|
|
76
|
+
}
|
|
77
|
+
if (state.data) {
|
|
78
|
+
yield {
|
|
79
|
+
event: state.event || "message",
|
|
80
|
+
data: state.data.trimEnd(),
|
|
81
|
+
id: state.id,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
reader.releaseLock();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function processSseEvent(event, workflowId, store) {
|
|
90
|
+
if (event.event === "heartbeat")
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
const data = JSON.parse(event.data);
|
|
94
|
+
if (event.event === "workflow.progress") {
|
|
95
|
+
const step = data.current_step ?? data.phase ?? "unknown";
|
|
96
|
+
const status = data.status ?? "";
|
|
97
|
+
store.addMessage({
|
|
98
|
+
role: "agent",
|
|
99
|
+
content: `[${step}] ${status}${data.message ? `: ${data.message}` : ""}`,
|
|
100
|
+
agent: step,
|
|
101
|
+
});
|
|
102
|
+
// Update agents from step data
|
|
103
|
+
if (Array.isArray(data.steps)) {
|
|
104
|
+
store.setAgents(data.steps.map((st) => ({
|
|
105
|
+
id: String(st.role ?? ""),
|
|
106
|
+
role_id: String(st.role ?? ""),
|
|
107
|
+
queue: String(st.role ?? ""),
|
|
108
|
+
status: String(st.status === "completed" ? "idle" : st.status === "running" ? "active" : "idle"),
|
|
109
|
+
model: "",
|
|
110
|
+
active_sessions: st.status === "running" ? 1 : 0,
|
|
111
|
+
total_tokens_used: Number(st.tokens ?? 0),
|
|
112
|
+
current_workflow: st.status === "running" ? workflowId : null,
|
|
113
|
+
skill_packs: [],
|
|
114
|
+
})));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (event.event === "workflow.completed") {
|
|
118
|
+
store.addMessage({
|
|
119
|
+
role: "system",
|
|
120
|
+
content: `Workflow completed: ${data.status ?? "done"}`,
|
|
121
|
+
});
|
|
122
|
+
store.removeSseController(workflowId);
|
|
123
|
+
}
|
|
124
|
+
else if (event.event === "error") {
|
|
125
|
+
store.addMessage({
|
|
126
|
+
role: "error",
|
|
127
|
+
content: `Stream error: ${data.message ?? JSON.stringify(data)}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
store.addMessage({
|
|
132
|
+
role: "agent",
|
|
133
|
+
content: `[${event.event}] ${JSON.stringify(data).slice(0, 200)}`,
|
|
134
|
+
agent: "stream",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Non-JSON data
|
|
140
|
+
store.addMessage({
|
|
141
|
+
role: "agent",
|
|
142
|
+
content: `[${event.event}] ${event.data.slice(0, 200)}`,
|
|
143
|
+
agent: "stream",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Subscribe to a workflow's SSE stream and push updates to the store.
|
|
149
|
+
* Reconnects with exponential backoff on connection failure (max 5 retries).
|
|
150
|
+
* Returns an AbortController for cleanup.
|
|
151
|
+
*/
|
|
152
|
+
export function subscribeToWorkflow(apiUrl, workflowId, store) {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const url = `${apiUrl.replace(/\/$/, "")}/stream/workflows/${workflowId}`;
|
|
155
|
+
(async () => {
|
|
156
|
+
let attempts = 0;
|
|
157
|
+
while (!controller.signal.aborted) {
|
|
158
|
+
try {
|
|
159
|
+
attempts++;
|
|
160
|
+
for await (const event of sseStream(url, controller.signal)) {
|
|
161
|
+
processSseEvent(event, workflowId, store);
|
|
162
|
+
// Reset attempts on successful event — connection is healthy
|
|
163
|
+
attempts = 0;
|
|
164
|
+
}
|
|
165
|
+
// Stream ended normally (server closed connection)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (controller.signal.aborted)
|
|
170
|
+
return; // Normal cleanup
|
|
171
|
+
if (attempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
store.addMessage({
|
|
174
|
+
role: "error",
|
|
175
|
+
content: `SSE stream failed after ${MAX_RECONNECT_ATTEMPTS} attempts: ${msg}`,
|
|
176
|
+
});
|
|
177
|
+
store.removeSseController(workflowId);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const delay = BASE_RECONNECT_DELAY_MS * Math.pow(2, attempts - 1);
|
|
181
|
+
store.addMessage({
|
|
182
|
+
role: "system",
|
|
183
|
+
content: `Stream disconnected, reconnecting in ${(delay / 1000).toFixed(0)}s (attempt ${attempts}/${MAX_RECONNECT_ATTEMPTS})...`,
|
|
184
|
+
});
|
|
185
|
+
await new Promise((resolve) => {
|
|
186
|
+
const timer = setTimeout(resolve, delay);
|
|
187
|
+
// Allow abort to cancel the wait
|
|
188
|
+
controller.signal.addEventListener("abort", () => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
resolve();
|
|
191
|
+
}, { once: true });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
return controller;
|
|
197
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Zustand store for the Hydra TUI.
|
|
3
|
+
* All widgets read from it, all commands write to it.
|
|
4
|
+
*/
|
|
5
|
+
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
|
6
|
+
export interface ChatMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
role: "user" | "agent" | "system" | "error";
|
|
9
|
+
content: string;
|
|
10
|
+
timestamp: Date;
|
|
11
|
+
agent?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface WorkflowDetail {
|
|
14
|
+
id: string;
|
|
15
|
+
workflow_type: string;
|
|
16
|
+
status: string;
|
|
17
|
+
current_step: string | null;
|
|
18
|
+
steps: Array<{
|
|
19
|
+
role: string;
|
|
20
|
+
status: string;
|
|
21
|
+
started_at?: string;
|
|
22
|
+
completed_at?: string;
|
|
23
|
+
cost?: number;
|
|
24
|
+
tokens?: number;
|
|
25
|
+
score?: number;
|
|
26
|
+
}>;
|
|
27
|
+
input: Record<string, unknown>;
|
|
28
|
+
output: Record<string, unknown> | null;
|
|
29
|
+
artifacts: Array<{
|
|
30
|
+
path: string;
|
|
31
|
+
type: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
}>;
|
|
34
|
+
created_at: string;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
metrics: Record<string, unknown> | null;
|
|
37
|
+
}
|
|
38
|
+
export interface WorkflowSummary {
|
|
39
|
+
workflow_id: string;
|
|
40
|
+
workflow_type: string;
|
|
41
|
+
task_description: string | null;
|
|
42
|
+
status: string;
|
|
43
|
+
domain: string | null;
|
|
44
|
+
technologies: string[] | null;
|
|
45
|
+
complexity: string | null;
|
|
46
|
+
created_at: string;
|
|
47
|
+
completed_at: string | null;
|
|
48
|
+
duration_minutes: number | null;
|
|
49
|
+
current_step: string | null;
|
|
50
|
+
completed_steps: string[] | null;
|
|
51
|
+
}
|
|
52
|
+
export interface InboxItem {
|
|
53
|
+
id: string;
|
|
54
|
+
type: string;
|
|
55
|
+
workflow_id: string;
|
|
56
|
+
workflow_type: string;
|
|
57
|
+
title: string;
|
|
58
|
+
summary: string;
|
|
59
|
+
priority: string;
|
|
60
|
+
created_at: string;
|
|
61
|
+
agent_role: string | null;
|
|
62
|
+
approval_type: string;
|
|
63
|
+
status: string;
|
|
64
|
+
}
|
|
65
|
+
export interface AgentInfo {
|
|
66
|
+
id: string;
|
|
67
|
+
role_id: string;
|
|
68
|
+
queue: string;
|
|
69
|
+
status: string;
|
|
70
|
+
model: string;
|
|
71
|
+
active_sessions: number;
|
|
72
|
+
total_tokens_used: number;
|
|
73
|
+
current_workflow: string | null;
|
|
74
|
+
skill_packs: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface HydraStore {
|
|
77
|
+
messages: ChatMessage[];
|
|
78
|
+
maxHistory: number;
|
|
79
|
+
activeWorkflowId: string | null;
|
|
80
|
+
activeWorkflow: WorkflowDetail | null;
|
|
81
|
+
workflows: WorkflowSummary[];
|
|
82
|
+
inbox: InboxItem[];
|
|
83
|
+
agents: AgentInfo[];
|
|
84
|
+
apiUrl: string;
|
|
85
|
+
apiStatus: ConnectionStatus;
|
|
86
|
+
sseAbortControllers: Map<string, AbortController>;
|
|
87
|
+
addMessage: (msg: Omit<ChatMessage, "id" | "timestamp">) => void;
|
|
88
|
+
clearMessages: () => void;
|
|
89
|
+
setActiveWorkflowId: (id: string | null) => void;
|
|
90
|
+
setActiveWorkflow: (wf: WorkflowDetail | null) => void;
|
|
91
|
+
setWorkflows: (wfs: WorkflowSummary[]) => void;
|
|
92
|
+
setInbox: (items: InboxItem[]) => void;
|
|
93
|
+
setAgents: (agents: AgentInfo[]) => void;
|
|
94
|
+
setApiUrl: (url: string) => void;
|
|
95
|
+
setApiStatus: (status: ConnectionStatus) => void;
|
|
96
|
+
addSseController: (key: string, controller: AbortController) => void;
|
|
97
|
+
removeSseController: (key: string) => void;
|
|
98
|
+
abortAllSse: () => void;
|
|
99
|
+
}
|
|
100
|
+
export declare const useStore: import("zustand").UseBoundStore<import("zustand").StoreApi<HydraStore>>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Zustand store for the Hydra TUI.
|
|
3
|
+
* All widgets read from it, all commands write to it.
|
|
4
|
+
*/
|
|
5
|
+
import { create } from "zustand";
|
|
6
|
+
let messageCounter = 0;
|
|
7
|
+
export const useStore = create((set, get) => ({
|
|
8
|
+
messages: [],
|
|
9
|
+
maxHistory: 1000,
|
|
10
|
+
activeWorkflowId: null,
|
|
11
|
+
activeWorkflow: null,
|
|
12
|
+
workflows: [],
|
|
13
|
+
inbox: [],
|
|
14
|
+
agents: [],
|
|
15
|
+
apiUrl: "http://localhost:7070",
|
|
16
|
+
apiStatus: "disconnected",
|
|
17
|
+
sseAbortControllers: new Map(),
|
|
18
|
+
addMessage: (msg) => set((state) => {
|
|
19
|
+
const next = [
|
|
20
|
+
...state.messages,
|
|
21
|
+
{
|
|
22
|
+
...msg,
|
|
23
|
+
id: `msg-${Date.now()}-${++messageCounter}`,
|
|
24
|
+
timestamp: new Date(),
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
return {
|
|
28
|
+
messages: next.length > state.maxHistory
|
|
29
|
+
? next.slice(-state.maxHistory)
|
|
30
|
+
: next,
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
clearMessages: () => set({ messages: [] }),
|
|
34
|
+
setActiveWorkflowId: (id) => set({ activeWorkflowId: id }),
|
|
35
|
+
setActiveWorkflow: (wf) => set({ activeWorkflow: wf }),
|
|
36
|
+
setWorkflows: (wfs) => set({ workflows: wfs }),
|
|
37
|
+
setInbox: (items) => set({ inbox: items }),
|
|
38
|
+
setAgents: (agents) => set({ agents }),
|
|
39
|
+
setApiUrl: (url) => set({ apiUrl: url }),
|
|
40
|
+
setApiStatus: (status) => set({ apiStatus: status }),
|
|
41
|
+
addSseController: (key, controller) => set((state) => {
|
|
42
|
+
const next = new Map(state.sseAbortControllers);
|
|
43
|
+
// Abort existing controller for this key
|
|
44
|
+
const existing = next.get(key);
|
|
45
|
+
if (existing)
|
|
46
|
+
existing.abort();
|
|
47
|
+
next.set(key, controller);
|
|
48
|
+
return { sseAbortControllers: next };
|
|
49
|
+
}),
|
|
50
|
+
removeSseController: (key) => set((state) => {
|
|
51
|
+
const next = new Map(state.sseAbortControllers);
|
|
52
|
+
const existing = next.get(key);
|
|
53
|
+
if (existing)
|
|
54
|
+
existing.abort();
|
|
55
|
+
next.delete(key);
|
|
56
|
+
return { sseAbortControllers: next };
|
|
57
|
+
}),
|
|
58
|
+
abortAllSse: () => set((state) => {
|
|
59
|
+
for (const controller of state.sseAbortControllers.values()) {
|
|
60
|
+
controller.abort();
|
|
61
|
+
}
|
|
62
|
+
return { sseAbortControllers: new Map() };
|
|
63
|
+
}),
|
|
64
|
+
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent panel widget.
|
|
3
|
+
* Shows live status of each agent role in the active workflow.
|
|
4
|
+
*/
|
|
5
|
+
interface AgentStatus {
|
|
6
|
+
role: string;
|
|
7
|
+
status: "done" | "running" | "waiting" | "pending" | "failed";
|
|
8
|
+
duration?: string;
|
|
9
|
+
}
|
|
10
|
+
interface AgentPanelProps {
|
|
11
|
+
workflowId?: string;
|
|
12
|
+
agents?: AgentStatus[];
|
|
13
|
+
}
|
|
14
|
+
export declare function AgentPanel({ workflowId, agents }: AgentPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Agent panel widget.
|
|
4
|
+
* Shows live status of each agent role in the active workflow.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
const STATUS_ICONS = {
|
|
8
|
+
done: "✓",
|
|
9
|
+
running: "⏳",
|
|
10
|
+
waiting: "⏸",
|
|
11
|
+
pending: "○",
|
|
12
|
+
failed: "✗",
|
|
13
|
+
};
|
|
14
|
+
const STATUS_COLORS = {
|
|
15
|
+
done: "green",
|
|
16
|
+
running: "cyan",
|
|
17
|
+
waiting: "yellow",
|
|
18
|
+
pending: "gray",
|
|
19
|
+
failed: "red",
|
|
20
|
+
};
|
|
21
|
+
export function AgentPanel({ workflowId, agents = [] }) {
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, underline: true, children: "Agents" }), !workflowId ? (_jsx(Text, { dimColor: true, children: "No active workflow" })) : agents.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for agents..." })) : (agents.map((agent) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: STATUS_COLORS[agent.status], children: STATUS_ICONS[agent.status] }), _jsx(Text, { children: agent.role.padEnd(14) }), _jsx(Text, { dimColor: true, children: agent.status })] }, agent.role))))] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval modal widget.
|
|
3
|
+
* Displays approval requests from workflows with approve/deny/edit options.
|
|
4
|
+
*/
|
|
5
|
+
interface ApprovalModalProps {
|
|
6
|
+
agent: string;
|
|
7
|
+
action: string;
|
|
8
|
+
command: string;
|
|
9
|
+
risk: "LOW" | "MEDIUM" | "HIGH";
|
|
10
|
+
active?: boolean;
|
|
11
|
+
onApprove: () => void;
|
|
12
|
+
onDeny: () => void;
|
|
13
|
+
onEdit?: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function ApprovalModal({ agent, action, command, risk, active, onApprove, onDeny, onEdit, }: ApprovalModalProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Approval modal widget.
|
|
4
|
+
* Displays approval requests from workflows with approve/deny/edit options.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
const RISK_COLORS = {
|
|
8
|
+
LOW: "green",
|
|
9
|
+
MEDIUM: "yellow",
|
|
10
|
+
HIGH: "red",
|
|
11
|
+
};
|
|
12
|
+
export function ApprovalModal({ agent, action, command, risk, active = true, onApprove, onDeny, onEdit, }) {
|
|
13
|
+
useInput((input) => {
|
|
14
|
+
if (!active)
|
|
15
|
+
return;
|
|
16
|
+
if (input === "y")
|
|
17
|
+
onApprove();
|
|
18
|
+
if (input === "n")
|
|
19
|
+
onDeny();
|
|
20
|
+
if (input === "e" && onEdit)
|
|
21
|
+
onEdit();
|
|
22
|
+
}, { isActive: active });
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", padding: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "APPROVAL REQUIRED" }), _jsx(Text, {}), _jsxs(Text, { children: ["Agent: ", _jsx(Text, { bold: true, children: agent })] }), _jsxs(Text, { children: ["Action: ", action] }), _jsx(Text, {}), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { children: command }) }), _jsx(Text, {}), _jsxs(Text, { children: ["Risk: ", _jsx(Text, { color: RISK_COLORS[risk], children: risk })] }), _jsx(Text, {}), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "[y]" }), " Approve", " ", _jsx(Text, { color: "red", children: "[n]" }), " Deny", " ", onEdit && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "blue", children: "[e]" }), " Edit"] }))] })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact tree widget.
|
|
3
|
+
* Displays a tree view of artifacts produced by the active workflow.
|
|
4
|
+
*/
|
|
5
|
+
interface Artifact {
|
|
6
|
+
path: string;
|
|
7
|
+
type: "file" | "directory";
|
|
8
|
+
}
|
|
9
|
+
interface ArtifactTreeProps {
|
|
10
|
+
workflowId?: string;
|
|
11
|
+
artifacts?: Artifact[];
|
|
12
|
+
}
|
|
13
|
+
export declare function ArtifactTree({ workflowId, artifacts }: ArtifactTreeProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Artifact tree widget.
|
|
4
|
+
* Displays a tree view of artifacts produced by the active workflow.
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
export function ArtifactTree({ workflowId, artifacts = [] }) {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, underline: true, children: "Artifacts" }), !workflowId ? (_jsx(Text, { dimColor: true, children: "No active workflow" })) : artifacts.length === 0 ? (_jsx(Text, { dimColor: true, children: "No artifacts yet" })) : (artifacts.map((artifact) => (_jsxs(Text, { children: [artifact.type === "directory" ? "📁 " : "📄 ", artifact.path] }, artifact.path))))] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat panel widget.
|
|
3
|
+
* Displays streaming output from agents, approval prompts, and user messages.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChatMessage } from "../store.js";
|
|
6
|
+
interface ChatPanelProps {
|
|
7
|
+
messages?: ChatMessage[];
|
|
8
|
+
}
|
|
9
|
+
export declare function ChatPanel({ messages }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|