herm-tui 1.0.0-dev.1
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/LICENSE +21 -0
- package/README.md +54 -0
- package/package.json +82 -0
- package/scripts/postinstall.ts +29 -0
- package/src/app/gateway.tsx +83 -0
- package/src/app/gatewayEvents.ts +203 -0
- package/src/app/launch.ts +41 -0
- package/src/app/skin.tsx +31 -0
- package/src/app/spawnHistory.ts +75 -0
- package/src/app/tabs.ts +23 -0
- package/src/app/turnReducer.ts +390 -0
- package/src/app/useAppKeys.ts +268 -0
- package/src/app/useAtRefPopover.ts +99 -0
- package/src/app/useInputHistory.ts +66 -0
- package/src/app/useSession.ts +102 -0
- package/src/app/useSlashCommands.ts +70 -0
- package/src/app/useSlashPopover.ts +48 -0
- package/src/app.tsx +917 -0
- package/src/commands/slash.ts +151 -0
- package/src/components/avatar/AnimatedAvatar.tsx +66 -0
- package/src/components/avatar/eikon.ts +144 -0
- package/src/components/avatar/states/error.ts +1155 -0
- package/src/components/avatar/states/idle.ts +1155 -0
- package/src/components/avatar/states/index.ts +30 -0
- package/src/components/avatar/states/listening.ts +1155 -0
- package/src/components/avatar/states/speaking.ts +1155 -0
- package/src/components/avatar/states/thinking.ts +1155 -0
- package/src/components/avatar/states/working.ts +1155 -0
- package/src/components/chat/AtRefPopover.tsx +54 -0
- package/src/components/chat/CodeBlock.tsx +67 -0
- package/src/components/chat/Composer.tsx +347 -0
- package/src/components/chat/DiffBlock.tsx +116 -0
- package/src/components/chat/ErrorBlock.tsx +70 -0
- package/src/components/chat/MediaChip.tsx +114 -0
- package/src/components/chat/MessageItem.tsx +282 -0
- package/src/components/chat/MessageList.tsx +114 -0
- package/src/components/chat/PromptCard.tsx +359 -0
- package/src/components/chat/SlashPopover.tsx +158 -0
- package/src/components/chat/ThoughtCloud.tsx +185 -0
- package/src/components/chat/TypingIndicator.tsx +25 -0
- package/src/components/chat/tool/Subagent.tsx +75 -0
- package/src/components/chat/tool/frame.tsx +69 -0
- package/src/components/chat/tool/index.tsx +65 -0
- package/src/components/chat/tool/preview.ts +57 -0
- package/src/components/sidebar/ContextGauge.tsx +102 -0
- package/src/components/sidebar/Sidebar.tsx +143 -0
- package/src/components/tabs/TabBar.tsx +50 -0
- package/src/components/ui/FileLink.tsx +52 -0
- package/src/config/index.ts +156 -0
- package/src/config/lane.ts +161 -0
- package/src/config/models.ts +95 -0
- package/src/config/rules.ts +80 -0
- package/src/config/schema.ts +308 -0
- package/src/dialogs/alert.tsx +52 -0
- package/src/dialogs/chafa.tsx +72 -0
- package/src/dialogs/confirm.tsx +58 -0
- package/src/dialogs/curator.tsx +153 -0
- package/src/dialogs/eikon-picker.tsx +95 -0
- package/src/dialogs/help.tsx +80 -0
- package/src/dialogs/history.tsx +92 -0
- package/src/dialogs/info.tsx +115 -0
- package/src/dialogs/keys.tsx +170 -0
- package/src/dialogs/logs.tsx +42 -0
- package/src/dialogs/message.tsx +38 -0
- package/src/dialogs/model-picker.tsx +123 -0
- package/src/dialogs/new-profile.tsx +69 -0
- package/src/dialogs/new-task.tsx +103 -0
- package/src/dialogs/profile.tsx +55 -0
- package/src/dialogs/rollback.tsx +190 -0
- package/src/dialogs/spawn-history.tsx +80 -0
- package/src/dialogs/text-prompt.tsx +68 -0
- package/src/dialogs/theme-picker.tsx +50 -0
- package/src/home/index.ts +23 -0
- package/src/home/store.ts +267 -0
- package/src/index.tsx +113 -0
- package/src/keys/catalog.ts +115 -0
- package/src/keys/chord.ts +125 -0
- package/src/keys/conflicts.ts +48 -0
- package/src/keys/context.tsx +112 -0
- package/src/keys/index.ts +5 -0
- package/src/keys/list.ts +94 -0
- package/src/keys/oc-compat.ts +87 -0
- package/src/tabs/Agents.tsx +607 -0
- package/src/tabs/Analytics.tsx +154 -0
- package/src/tabs/Chat.tsx +50 -0
- package/src/tabs/Config.tsx +605 -0
- package/src/tabs/Context.tsx +599 -0
- package/src/tabs/Cron.tsx +294 -0
- package/src/tabs/Env.tsx +227 -0
- package/src/tabs/Kanban.tsx +367 -0
- package/src/tabs/Memory.tsx +294 -0
- package/src/tabs/Sessions.tsx +786 -0
- package/src/tabs/Skills.tsx +507 -0
- package/src/tabs/Toolsets.tsx +266 -0
- package/src/theme/builtin.ts +78 -0
- package/src/theme/context.tsx +106 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/resolve.ts +134 -0
- package/src/theme/syntax.ts +31 -0
- package/src/theme/themes/aura.json +69 -0
- package/src/theme/themes/ayu.json +80 -0
- package/src/theme/themes/carbonfox.json +248 -0
- package/src/theme/themes/catppuccin-frappe.json +233 -0
- package/src/theme/themes/catppuccin-macchiato.json +233 -0
- package/src/theme/themes/catppuccin.json +112 -0
- package/src/theme/themes/cobalt2.json +228 -0
- package/src/theme/themes/cursor.json +249 -0
- package/src/theme/themes/dracula.json +219 -0
- package/src/theme/themes/everforest.json +241 -0
- package/src/theme/themes/flexoki.json +237 -0
- package/src/theme/themes/github.json +233 -0
- package/src/theme/themes/gruvbox.json +242 -0
- package/src/theme/themes/kanagawa.json +77 -0
- package/src/theme/themes/lucent-orng.json +237 -0
- package/src/theme/themes/material.json +235 -0
- package/src/theme/themes/matrix.json +77 -0
- package/src/theme/themes/mercury.json +252 -0
- package/src/theme/themes/monokai.json +221 -0
- package/src/theme/themes/nightowl.json +221 -0
- package/src/theme/themes/nord.json +223 -0
- package/src/theme/themes/one-dark.json +84 -0
- package/src/theme/themes/opencode.json +245 -0
- package/src/theme/themes/orng.json +249 -0
- package/src/theme/themes/osaka-jade.json +93 -0
- package/src/theme/themes/palenight.json +222 -0
- package/src/theme/themes/rosepine.json +234 -0
- package/src/theme/themes/solarized.json +223 -0
- package/src/theme/themes/synthwave84.json +226 -0
- package/src/theme/themes/tokyonight.json +243 -0
- package/src/theme/themes/vercel.json +245 -0
- package/src/theme/themes/vesper.json +218 -0
- package/src/theme/themes/zenburn.json +223 -0
- package/src/theme/types.ts +119 -0
- package/src/types/message.ts +97 -0
- package/src/ui/ChafaImage.tsx +64 -0
- package/src/ui/Splash.tsx +118 -0
- package/src/ui/borders.ts +28 -0
- package/src/ui/command.tsx +104 -0
- package/src/ui/dialog-select.tsx +164 -0
- package/src/ui/dialog.tsx +102 -0
- package/src/ui/fmt.ts +82 -0
- package/src/ui/kv.tsx +28 -0
- package/src/ui/shell.tsx +45 -0
- package/src/ui/spinner.tsx +59 -0
- package/src/ui/splash-art.ts +123 -0
- package/src/ui/table.tsx +117 -0
- package/src/ui/ticker.tsx +90 -0
- package/src/ui/toast.tsx +130 -0
- package/src/utils/categorical.ts +77 -0
- package/src/utils/chafa.ts +173 -0
- package/src/utils/clipboard.ts +67 -0
- package/src/utils/context-segments.ts +317 -0
- package/src/utils/control.ts +495 -0
- package/src/utils/drop.ts +25 -0
- package/src/utils/editor.ts +33 -0
- package/src/utils/fuzzy.ts +45 -0
- package/src/utils/gateway-client.ts +253 -0
- package/src/utils/gateway-types.ts +282 -0
- package/src/utils/git.ts +57 -0
- package/src/utils/hermes-analytics.ts +134 -0
- package/src/utils/hermes-home.ts +821 -0
- package/src/utils/hermes-kanban.ts +154 -0
- package/src/utils/hermes-profiles.ts +217 -0
- package/src/utils/interpolate.ts +31 -0
- package/src/utils/math-unicode.ts +818 -0
- package/src/utils/memory-activity.ts +140 -0
- package/src/utils/open-file.ts +13 -0
- package/src/utils/paths.ts +52 -0
- package/src/utils/perf.ts +235 -0
- package/src/utils/preferences.ts +150 -0
- package/src/utils/sessions-db.ts +396 -0
- package/src/utils/subagent-tree.ts +146 -0
- package/src/utils/terminal-reset.ts +129 -0
- package/src/utils/tips.ts +67 -0
- package/src/utils/tokens.ts +87 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Stdio JSON-RPC 2.0 client for tui_gateway. Spawns the gateway as a child
|
|
2
|
+
// process and speaks newline-delimited JSON on stdin/stdout.
|
|
3
|
+
|
|
4
|
+
import { EventEmitter } from "events"
|
|
5
|
+
import { homedir } from "os"
|
|
6
|
+
import { resolve, delimiter } from "path"
|
|
7
|
+
import { existsSync } from "fs"
|
|
8
|
+
import type { GatewayEvent } from "./gateway-types"
|
|
9
|
+
|
|
10
|
+
const LOG_MAX = 200
|
|
11
|
+
const LOG_PREVIEW = 240
|
|
12
|
+
const STARTUP_MS = 15_000
|
|
13
|
+
const REQUEST_MS = 120_000
|
|
14
|
+
|
|
15
|
+
/** Locate the hermes-agent source tree (gateway + hermes_cli live here).
|
|
16
|
+
* Default: ~/.hermes/hermes-agent (where `hermes update` installs it).
|
|
17
|
+
* Override with HERMES_AGENT_ROOT for dev clones / exotic layouts. */
|
|
18
|
+
export function hermesAgentRoot(): string {
|
|
19
|
+
if (process.env.HERMES_AGENT_ROOT) return process.env.HERMES_AGENT_ROOT
|
|
20
|
+
const home = process.env.HOME || homedir()
|
|
21
|
+
return `${home}/.hermes/hermes-agent`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Pending = {
|
|
25
|
+
resolve: (v: unknown) => void
|
|
26
|
+
reject: (e: Error) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function python(root: string): string {
|
|
30
|
+
const env = process.env.HERMES_PYTHON?.trim()
|
|
31
|
+
if (env) return env
|
|
32
|
+
|
|
33
|
+
const venv = process.env.VIRTUAL_ENV?.trim()
|
|
34
|
+
const paths = [
|
|
35
|
+
venv && resolve(venv, "bin/python"),
|
|
36
|
+
resolve(root, "venv/bin/python"),
|
|
37
|
+
resolve(root, "venv/bin/python3"),
|
|
38
|
+
resolve(root, ".venv/bin/python"),
|
|
39
|
+
resolve(root, ".venv/bin/python3"),
|
|
40
|
+
]
|
|
41
|
+
return paths.find(p => p && existsSync(p)) || "python3"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function asEvent(v: unknown): GatewayEvent | null {
|
|
45
|
+
if (v && typeof v === "object" && !Array.isArray(v) && typeof (v as { type?: unknown }).type === "string")
|
|
46
|
+
return v as GatewayEvent
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read lines from a ReadableStream (Bun subprocess stdout/stderr)
|
|
51
|
+
async function lines(stream: ReadableStream<Uint8Array>, cb: (line: string) => void) {
|
|
52
|
+
const reader = stream.getReader()
|
|
53
|
+
const decoder = new TextDecoder()
|
|
54
|
+
let buf = ""
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read()
|
|
58
|
+
if (done) break
|
|
59
|
+
buf += decoder.decode(value, { stream: true })
|
|
60
|
+
const parts = buf.split("\n")
|
|
61
|
+
buf = parts.pop() || ""
|
|
62
|
+
for (const line of parts) {
|
|
63
|
+
if (line) cb(line)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Flush remaining
|
|
67
|
+
if (buf.trim()) cb(buf)
|
|
68
|
+
} catch {
|
|
69
|
+
// Stream closed
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class GatewayClient extends EventEmitter {
|
|
74
|
+
private proc: ReturnType<typeof Bun.spawn> | null = null
|
|
75
|
+
private id = 0
|
|
76
|
+
private logs: string[] = []
|
|
77
|
+
private pending = new Map<string, Pending>()
|
|
78
|
+
private buf: GatewayEvent[] = []
|
|
79
|
+
private exit: number | null | undefined
|
|
80
|
+
private ok = false
|
|
81
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
82
|
+
private sub = false
|
|
83
|
+
|
|
84
|
+
private root(): string { return hermesAgentRoot() }
|
|
85
|
+
|
|
86
|
+
private push(ev: GatewayEvent) {
|
|
87
|
+
if (ev.type === "gateway.ready") {
|
|
88
|
+
this.ok = true
|
|
89
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null }
|
|
90
|
+
}
|
|
91
|
+
if (this.sub) return void this.emit("event", ev)
|
|
92
|
+
this.buf.push(ev)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private log(line: string) {
|
|
96
|
+
if (this.logs.push(line) > LOG_MAX) this.logs.splice(0, this.logs.length - LOG_MAX)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private dispatch(msg: Record<string, unknown>) {
|
|
100
|
+
const id = msg.id as string | undefined
|
|
101
|
+
const p = id ? this.pending.get(id) : undefined
|
|
102
|
+
|
|
103
|
+
if (p) {
|
|
104
|
+
this.pending.delete(id!)
|
|
105
|
+
if (msg.error) {
|
|
106
|
+
const err = msg.error as { message?: unknown }
|
|
107
|
+
p.reject(new Error(typeof err?.message === "string" ? err.message : "request failed"))
|
|
108
|
+
} else {
|
|
109
|
+
p.resolve(msg.result)
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (msg.method === "event") {
|
|
115
|
+
const ev = asEvent(msg.params)
|
|
116
|
+
if (ev) this.push(ev)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fail(err: Error) {
|
|
121
|
+
for (const p of this.pending.values()) p.reject(err)
|
|
122
|
+
this.pending.clear()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
start() {
|
|
126
|
+
const root = this.root()
|
|
127
|
+
const bin = python(root)
|
|
128
|
+
const cwd = process.env.HERMES_CWD || root
|
|
129
|
+
const env = { ...process.env } as Record<string, string>
|
|
130
|
+
const pp = env.PYTHONPATH?.trim()
|
|
131
|
+
env.PYTHONPATH = pp ? `${root}${delimiter}${pp}` : root
|
|
132
|
+
|
|
133
|
+
// Reset state
|
|
134
|
+
this.ok = false
|
|
135
|
+
this.buf = []
|
|
136
|
+
this.exit = undefined
|
|
137
|
+
|
|
138
|
+
if (this.proc) {
|
|
139
|
+
try { this.proc.kill() } catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.timer) clearTimeout(this.timer)
|
|
143
|
+
this.timer = setTimeout(() => {
|
|
144
|
+
if (this.ok) return
|
|
145
|
+
this.log(`[startup] timed out (python=${bin}, cwd=${cwd})`)
|
|
146
|
+
this.push({ type: "gateway.start_timeout", payload: { cwd, python: bin } })
|
|
147
|
+
}, STARTUP_MS)
|
|
148
|
+
|
|
149
|
+
const proc = Bun.spawn(["sh", "-c", `exec ${bin} -m tui_gateway.entry`], {
|
|
150
|
+
cwd,
|
|
151
|
+
env,
|
|
152
|
+
stdin: "pipe",
|
|
153
|
+
stdout: "pipe",
|
|
154
|
+
stderr: "pipe",
|
|
155
|
+
})
|
|
156
|
+
this.proc = proc
|
|
157
|
+
|
|
158
|
+
// Read stdout lines — Bun returns ReadableStream
|
|
159
|
+
if (this.proc.stdout) {
|
|
160
|
+
lines(this.proc.stdout as ReadableStream<Uint8Array>, raw => {
|
|
161
|
+
try {
|
|
162
|
+
this.dispatch(JSON.parse(raw))
|
|
163
|
+
} catch {
|
|
164
|
+
const preview = raw.trim().slice(0, LOG_PREVIEW) || "(empty)"
|
|
165
|
+
this.log(`[protocol] malformed: ${preview}`)
|
|
166
|
+
this.push({ type: "gateway.protocol_error", payload: { preview } })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Read stderr lines
|
|
172
|
+
if (this.proc.stderr) {
|
|
173
|
+
lines(this.proc.stderr as ReadableStream<Uint8Array>, raw => {
|
|
174
|
+
const line = raw.trim()
|
|
175
|
+
if (!line) return
|
|
176
|
+
this.log(line)
|
|
177
|
+
this.push({ type: "gateway.stderr", payload: { line } })
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Handle exit — guard against a superseded proc (restart kills the
|
|
182
|
+
// old one, whose exit handler must not touch the new proc's state).
|
|
183
|
+
proc.exited.then(code => {
|
|
184
|
+
if (this.proc !== proc) return
|
|
185
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null }
|
|
186
|
+
this.fail(new Error(`gateway exited${code === null ? "" : ` (${code})`}`))
|
|
187
|
+
if (this.sub) this.emit("exit", code)
|
|
188
|
+
else this.exit = code
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
drain() {
|
|
193
|
+
if (this.sub) return
|
|
194
|
+
this.sub = true
|
|
195
|
+
for (const ev of this.buf.splice(0)) this.emit("event", ev)
|
|
196
|
+
if (this.exit !== undefined) {
|
|
197
|
+
const code = this.exit
|
|
198
|
+
this.exit = undefined
|
|
199
|
+
this.emit("exit", code)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
tail(n = 20): string {
|
|
204
|
+
return this.logs.slice(-Math.max(1, n)).join("\n")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private sid = ""
|
|
208
|
+
|
|
209
|
+
/** Set the active session id; auto-injected into subsequent requests. */
|
|
210
|
+
setSession(sid: string) {
|
|
211
|
+
this.sid = sid
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
request<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
215
|
+
if (!this.proc || this.proc.exitCode !== null) this.start()
|
|
216
|
+
|
|
217
|
+
const stdin = this.proc?.stdin
|
|
218
|
+
if (!stdin || typeof stdin === "number") return Promise.reject(new Error("gateway not running"))
|
|
219
|
+
|
|
220
|
+
const rid = `r${++this.id}`
|
|
221
|
+
const writer = stdin as { write(data: string | Uint8Array): number }
|
|
222
|
+
const merged = this.sid && params.session_id === undefined
|
|
223
|
+
? { session_id: this.sid, ...params }
|
|
224
|
+
: params
|
|
225
|
+
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const timeout = setTimeout(() => {
|
|
228
|
+
if (this.pending.delete(rid)) reject(new Error(`timeout: ${method}`))
|
|
229
|
+
}, REQUEST_MS)
|
|
230
|
+
|
|
231
|
+
this.pending.set(rid, {
|
|
232
|
+
reject: e => { clearTimeout(timeout); reject(e) },
|
|
233
|
+
resolve: v => { clearTimeout(timeout); resolve(v as T) },
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
writer.write(JSON.stringify({ jsonrpc: "2.0", id: rid, method, params: merged }) + "\n")
|
|
238
|
+
} catch (e) {
|
|
239
|
+
clearTimeout(timeout)
|
|
240
|
+
this.pending.delete(rid)
|
|
241
|
+
reject(e instanceof Error ? e : new Error(String(e)))
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
kill() {
|
|
247
|
+
this.proc?.kill()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get ready(): boolean {
|
|
251
|
+
return this.ok
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Typed events and RPC responses for the tui_gateway JSON-RPC protocol.
|
|
2
|
+
|
|
3
|
+
import type { Usage } from "../types/message"
|
|
4
|
+
|
|
5
|
+
// ── Events (server → client) ────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type GatewayEvent =
|
|
8
|
+
| { type: "gateway.ready"; payload?: { skin?: GatewaySkin } }
|
|
9
|
+
| { type: "gateway.stderr"; payload: { line: string } }
|
|
10
|
+
| { type: "gateway.start_timeout"; payload?: { cwd?: string; python?: string } }
|
|
11
|
+
| { type: "gateway.protocol_error"; payload?: { preview?: string } }
|
|
12
|
+
| { type: "session.info"; payload: SessionInfo }
|
|
13
|
+
| { type: "skin.changed"; payload?: GatewaySkin }
|
|
14
|
+
| { type: "message.start"; payload?: undefined }
|
|
15
|
+
| { type: "message.delta"; payload?: { text?: string; rendered?: string } }
|
|
16
|
+
| { type: "message.complete"; payload?: { text?: string | null; rendered?: string; reasoning?: string; status?: "complete" | "error" | "interrupted"; usage?: Usage } }
|
|
17
|
+
| { type: "thinking.delta"; payload?: { text?: string } }
|
|
18
|
+
| { type: "reasoning.delta"; payload?: { text?: string } }
|
|
19
|
+
| { type: "reasoning.available"; payload?: { text?: string } }
|
|
20
|
+
| { type: "status.update"; payload?: { text?: string; kind?: string } }
|
|
21
|
+
| { type: "tool.start"; payload: { tool_id: string; name?: string; context?: string } }
|
|
22
|
+
| { type: "tool.progress"; payload: { name?: string; preview?: string } }
|
|
23
|
+
| { type: "tool.generating"; payload: { name?: string } }
|
|
24
|
+
| { type: "tool.complete"; payload: { tool_id: string; name?: string; summary?: string; error?: string; inline_diff?: string } }
|
|
25
|
+
| { type: "clarify.request"; payload: { request_id: string; question: string; choices: string[] | null } }
|
|
26
|
+
| { type: "approval.request"; payload: { command: string; description: string } }
|
|
27
|
+
| { type: "sudo.request"; payload: { request_id: string } }
|
|
28
|
+
| { type: "secret.request"; payload: { request_id: string; prompt: string; env_var: string } }
|
|
29
|
+
| { type: "background.complete"; payload: { task_id: string; text: string } }
|
|
30
|
+
| { type: "review.summary"; payload?: { text?: string } }
|
|
31
|
+
| { type: "btw.complete"; payload: { text: string } }
|
|
32
|
+
| { type: "browser.progress"; payload?: { message?: string; level?: "info" | "error" } }
|
|
33
|
+
| { type: "subagent.start"; payload: SubagentPayload }
|
|
34
|
+
| { type: "subagent.thinking"; payload: SubagentPayload }
|
|
35
|
+
| { type: "subagent.tool"; payload: SubagentPayload }
|
|
36
|
+
| { type: "subagent.progress"; payload: SubagentPayload }
|
|
37
|
+
| { type: "subagent.complete"; payload: SubagentPayload }
|
|
38
|
+
| { type: "error"; payload?: { message?: string } }
|
|
39
|
+
|
|
40
|
+
export type SubagentPayload = {
|
|
41
|
+
task_index: number
|
|
42
|
+
goal: string
|
|
43
|
+
task_count?: number
|
|
44
|
+
status?: "running" | "completed" | "failed" | "interrupted"
|
|
45
|
+
text?: string
|
|
46
|
+
tool_name?: string
|
|
47
|
+
tool_preview?: string
|
|
48
|
+
summary?: string
|
|
49
|
+
duration_seconds?: number
|
|
50
|
+
// Spawn-tree identity (upstream delegate_tool threads these through
|
|
51
|
+
// every subagent.* event). All optional — absence falls back to flat
|
|
52
|
+
// task_index keying.
|
|
53
|
+
subagent_id?: string
|
|
54
|
+
parent_id?: string
|
|
55
|
+
depth?: number
|
|
56
|
+
model?: string
|
|
57
|
+
tool_count?: number
|
|
58
|
+
toolsets?: string[]
|
|
59
|
+
// Rollups on subagent.complete
|
|
60
|
+
input_tokens?: number
|
|
61
|
+
output_tokens?: number
|
|
62
|
+
reasoning_tokens?: number
|
|
63
|
+
api_calls?: number
|
|
64
|
+
cost_usd?: number
|
|
65
|
+
files_read?: string[]
|
|
66
|
+
files_written?: string[]
|
|
67
|
+
output_tail?: Array<{ tool: string; preview: string; is_error?: boolean }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// delegation.status response — list_active_subagents() snapshot plus
|
|
71
|
+
// scheduler flags. Records are a copy of the live registry minus the
|
|
72
|
+
// agent handle.
|
|
73
|
+
export type DelegationRecord = {
|
|
74
|
+
subagent_id: string
|
|
75
|
+
parent_id?: string | null
|
|
76
|
+
depth: number
|
|
77
|
+
goal: string
|
|
78
|
+
model?: string
|
|
79
|
+
started_at?: number
|
|
80
|
+
tool_count?: number
|
|
81
|
+
status?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type DelegationStatus = {
|
|
85
|
+
active: DelegationRecord[]
|
|
86
|
+
paused: boolean
|
|
87
|
+
max_spawn_depth: number
|
|
88
|
+
max_concurrent_children: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// spawn_tree.list index entries + spawn_tree.load payload
|
|
92
|
+
export type SpawnTreeEntry = {
|
|
93
|
+
path: string
|
|
94
|
+
session_id: string
|
|
95
|
+
label: string
|
|
96
|
+
count: number
|
|
97
|
+
started_at?: number | null
|
|
98
|
+
finished_at: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type SpawnTreeSnapshot = {
|
|
102
|
+
session_id?: string
|
|
103
|
+
label?: string
|
|
104
|
+
started_at?: number | null
|
|
105
|
+
finished_at?: number
|
|
106
|
+
subagents: SpawnSubagent[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persisted per-subagent record — the shape we save, and the shape
|
|
110
|
+
// spawn_tree.load round-trips. A completed SubagentPayload superset.
|
|
111
|
+
export type SpawnSubagent = {
|
|
112
|
+
subagent_id: string
|
|
113
|
+
parent_id?: string | null
|
|
114
|
+
depth: number
|
|
115
|
+
goal: string
|
|
116
|
+
model?: string
|
|
117
|
+
started_at: number
|
|
118
|
+
finished_at?: number
|
|
119
|
+
tool_count: number
|
|
120
|
+
status: "running" | "completed" | "failed" | "interrupted"
|
|
121
|
+
input_tokens?: number
|
|
122
|
+
output_tokens?: number
|
|
123
|
+
cost_usd?: number
|
|
124
|
+
trail?: Array<{ name: string; preview?: string }>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type GatewaySkin = {
|
|
128
|
+
name?: string
|
|
129
|
+
colors?: Record<string, string>
|
|
130
|
+
branding?: Record<string, string>
|
|
131
|
+
banner_hero?: string
|
|
132
|
+
banner_logo?: string
|
|
133
|
+
tool_prefix?: string
|
|
134
|
+
help_header?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type McpServer = {
|
|
138
|
+
name: string
|
|
139
|
+
transport: string
|
|
140
|
+
tools: number
|
|
141
|
+
connected: boolean
|
|
142
|
+
error?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type SessionInfo = {
|
|
146
|
+
model?: string
|
|
147
|
+
cwd?: string
|
|
148
|
+
session_id?: string
|
|
149
|
+
tools?: Record<string, string[]>
|
|
150
|
+
skills?: Record<string, string[]>
|
|
151
|
+
version?: string
|
|
152
|
+
/**
|
|
153
|
+
* Wire usage payload for the current session. Server builds this via
|
|
154
|
+
* `_get_usage(agent)` (tui_gateway/server.py:826), which extends the
|
|
155
|
+
* base Usage with ctx/compression fields when a ContextCompressor is
|
|
156
|
+
* attached — so `compressions`/`context_used`/`context_max`/
|
|
157
|
+
* `context_percent` may be present. Intersection type keeps both
|
|
158
|
+
* shapes satisfied.
|
|
159
|
+
*/
|
|
160
|
+
usage?: Usage & {
|
|
161
|
+
context_used?: number
|
|
162
|
+
context_max?: number
|
|
163
|
+
context_percent?: number
|
|
164
|
+
compressions?: number
|
|
165
|
+
}
|
|
166
|
+
context_max?: number
|
|
167
|
+
context_used?: number
|
|
168
|
+
credential_warning?: string
|
|
169
|
+
mcp_servers?: McpServer[]
|
|
170
|
+
/** hermes-agent version string (e.g. "1.14.2-dev+abc123") */
|
|
171
|
+
release_date?: string
|
|
172
|
+
/** commits behind origin/main; null = unknown, 0 = up to date */
|
|
173
|
+
update_behind?: number | null
|
|
174
|
+
/** platform-appropriate update invocation */
|
|
175
|
+
update_command?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── RPC responses ───────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
export type SessionCreateResponse = {
|
|
181
|
+
session_id: string
|
|
182
|
+
info?: SessionInfo & { credential_warning?: string }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type SessionResumeResponse = {
|
|
186
|
+
session_id: string
|
|
187
|
+
resumed?: string
|
|
188
|
+
messages: TranscriptMessage[]
|
|
189
|
+
message_count?: number
|
|
190
|
+
info?: SessionInfo
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type SessionListItem = {
|
|
194
|
+
id: string
|
|
195
|
+
title: string
|
|
196
|
+
preview: string
|
|
197
|
+
message_count: number
|
|
198
|
+
started_at: number
|
|
199
|
+
source?: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type SessionListResponse = {
|
|
203
|
+
sessions?: SessionListItem[]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type SessionUsageResponse = {
|
|
207
|
+
model?: string
|
|
208
|
+
calls?: number
|
|
209
|
+
input?: number
|
|
210
|
+
output?: number
|
|
211
|
+
total?: number
|
|
212
|
+
cache_read?: number
|
|
213
|
+
cache_write?: number
|
|
214
|
+
cost_usd?: number
|
|
215
|
+
cost_status?: "estimated" | "exact"
|
|
216
|
+
context_used?: number
|
|
217
|
+
context_max?: number
|
|
218
|
+
context_percent?: number
|
|
219
|
+
compressions?: number
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Content part inside a multimodal user turn — upstream stores the raw
|
|
223
|
+
* OpenAI content list for native-mode image routing. We only care about
|
|
224
|
+
* flattening the text fragments back into a string for render. */
|
|
225
|
+
export type ContentPart =
|
|
226
|
+
| { type: "text"; text: string }
|
|
227
|
+
| { type: "image_url"; image_url: { url: string } }
|
|
228
|
+
| { type: string }
|
|
229
|
+
|
|
230
|
+
export type TranscriptMessage = {
|
|
231
|
+
role: "user" | "assistant" | "system" | "tool"
|
|
232
|
+
/** Either a plain string (text-mode, assistant, system) or a list of
|
|
233
|
+
* OpenAI content parts (native-mode user turns with attached images). */
|
|
234
|
+
text?: string | ContentPart[]
|
|
235
|
+
name?: string
|
|
236
|
+
context?: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export type CommandsCatalogResponse = {
|
|
240
|
+
categories?: Array<{ name: string; pairs?: [string, string][] }>
|
|
241
|
+
pairs?: [string, string][]
|
|
242
|
+
canon?: Record<string, string>
|
|
243
|
+
sub?: Record<string, string[]>
|
|
244
|
+
skill_count?: number
|
|
245
|
+
warning?: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type ConfigSetResponse = {
|
|
249
|
+
value?: string
|
|
250
|
+
info?: SessionInfo
|
|
251
|
+
warning?: string
|
|
252
|
+
history_reset?: boolean
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type ModelOptionsResponse = {
|
|
256
|
+
provider?: string
|
|
257
|
+
model?: string
|
|
258
|
+
providers?: {
|
|
259
|
+
slug: string
|
|
260
|
+
name: string
|
|
261
|
+
models?: string[]
|
|
262
|
+
total_models?: number
|
|
263
|
+
is_current?: boolean
|
|
264
|
+
warning?: string
|
|
265
|
+
}[]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export type ImageAttachResponse = {
|
|
269
|
+
attached: boolean
|
|
270
|
+
path?: string
|
|
271
|
+
count?: number
|
|
272
|
+
name?: string
|
|
273
|
+
width?: number
|
|
274
|
+
height?: number
|
|
275
|
+
token_estimate?: number
|
|
276
|
+
message?: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type DropDetectResponse =
|
|
280
|
+
| { matched: false }
|
|
281
|
+
| ({ matched: true; is_image: true; text: string } & Omit<ImageAttachResponse, "attached" | "message">)
|
|
282
|
+
| { matched: true; is_image: false; path: string; name: string; text: string }
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Git branch for the sidebar. One-shot resolve + fs.watch on
|
|
2
|
+
// `<gitdir>/HEAD` so checkout/switch is picked up without polling.
|
|
3
|
+
// Ink's equivalent polls every 15s; the watcher is strictly cheaper
|
|
4
|
+
// and fires exactly on the event that matters.
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState } from "react"
|
|
7
|
+
import { watch, type FSWatcher } from "node:fs"
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = 500
|
|
10
|
+
|
|
11
|
+
async function git(cwd: string, ...args: string[]): Promise<string | null> {
|
|
12
|
+
const p = Bun.spawn(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" })
|
|
13
|
+
const t = setTimeout(() => p.kill(), TIMEOUT)
|
|
14
|
+
const out = await new Response(p.stdout).text()
|
|
15
|
+
clearTimeout(t)
|
|
16
|
+
return (await p.exited) === 0 ? out.trim() : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Branch name for `cwd`, or null when not in a repo / detached HEAD. */
|
|
20
|
+
export async function branch(cwd: string): Promise<string | null> {
|
|
21
|
+
const b = await git(cwd, "rev-parse", "--abbrev-ref", "HEAD")
|
|
22
|
+
return !b || b === "HEAD" ? null : b
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Absolute .git dir for `cwd` (handles worktrees via git's own resolver). */
|
|
26
|
+
export async function gitdir(cwd: string): Promise<string | null> {
|
|
27
|
+
return git(cwd, "rev-parse", "--absolute-git-dir")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useGitBranch(cwd: string | undefined): string | null {
|
|
31
|
+
const [val, set] = useState<string | null>(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!cwd) { set(null); return }
|
|
35
|
+
let dead = false
|
|
36
|
+
let w: FSWatcher | undefined
|
|
37
|
+
const read = () => branch(cwd).then(b => { if (!dead) set(b) })
|
|
38
|
+
void read()
|
|
39
|
+
// HEAD is rewritten (not edited in-place) on checkout — watch the
|
|
40
|
+
// parent dir and filter on basename so rename-into-place fires.
|
|
41
|
+
gitdir(cwd).then(dir => {
|
|
42
|
+
if (dead || !dir) return
|
|
43
|
+
try {
|
|
44
|
+
w = watch(dir, { persistent: false }, (_ev, f) => {
|
|
45
|
+
if (f === "HEAD") void read()
|
|
46
|
+
})
|
|
47
|
+
} catch { /* unwatchable fs */ }
|
|
48
|
+
})
|
|
49
|
+
return () => { dead = true; w?.close() }
|
|
50
|
+
}, [cwd])
|
|
51
|
+
|
|
52
|
+
return val
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Right-ellipsise keeping the discriminating tail (feature/foo → …e/foo). */
|
|
56
|
+
export const rtrunc = (s: string, max: number) =>
|
|
57
|
+
s.length <= max ? s : "…" + s.slice(-(max - 1))
|