herm-tui 1.0.0-dev.1 → 1.0.0-dev.3

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 (188) hide show
  1. package/db.worker.js +81 -0
  2. package/highlights-eq9cgrbb.scm +604 -0
  3. package/highlights-ghv9g403.scm +205 -0
  4. package/highlights-hk7bwhj4.scm +284 -0
  5. package/highlights-r812a2qc.scm +150 -0
  6. package/highlights-x6tmsnaa.scm +115 -0
  7. package/index.js +10374 -0
  8. package/injections-73j83es3.scm +27 -0
  9. package/package.json +14 -64
  10. package/parser.worker.js +8 -0
  11. package/tree-sitter-3jzf13jk.wasm +0 -0
  12. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/scripts/postinstall.ts +0 -29
  18. package/src/app/gateway.tsx +0 -83
  19. package/src/app/gatewayEvents.ts +0 -203
  20. package/src/app/launch.ts +0 -41
  21. package/src/app/skin.tsx +0 -31
  22. package/src/app/spawnHistory.ts +0 -75
  23. package/src/app/tabs.ts +0 -23
  24. package/src/app/turnReducer.ts +0 -390
  25. package/src/app/useAppKeys.ts +0 -268
  26. package/src/app/useAtRefPopover.ts +0 -99
  27. package/src/app/useInputHistory.ts +0 -66
  28. package/src/app/useSession.ts +0 -102
  29. package/src/app/useSlashCommands.ts +0 -70
  30. package/src/app/useSlashPopover.ts +0 -48
  31. package/src/app.tsx +0 -917
  32. package/src/commands/slash.ts +0 -151
  33. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  34. package/src/components/avatar/eikon.ts +0 -144
  35. package/src/components/avatar/states/error.ts +0 -1155
  36. package/src/components/avatar/states/idle.ts +0 -1155
  37. package/src/components/avatar/states/index.ts +0 -30
  38. package/src/components/avatar/states/listening.ts +0 -1155
  39. package/src/components/avatar/states/speaking.ts +0 -1155
  40. package/src/components/avatar/states/thinking.ts +0 -1155
  41. package/src/components/avatar/states/working.ts +0 -1155
  42. package/src/components/chat/AtRefPopover.tsx +0 -54
  43. package/src/components/chat/CodeBlock.tsx +0 -67
  44. package/src/components/chat/Composer.tsx +0 -347
  45. package/src/components/chat/DiffBlock.tsx +0 -116
  46. package/src/components/chat/ErrorBlock.tsx +0 -70
  47. package/src/components/chat/MediaChip.tsx +0 -114
  48. package/src/components/chat/MessageItem.tsx +0 -282
  49. package/src/components/chat/MessageList.tsx +0 -114
  50. package/src/components/chat/PromptCard.tsx +0 -359
  51. package/src/components/chat/SlashPopover.tsx +0 -158
  52. package/src/components/chat/ThoughtCloud.tsx +0 -185
  53. package/src/components/chat/TypingIndicator.tsx +0 -25
  54. package/src/components/chat/tool/Subagent.tsx +0 -75
  55. package/src/components/chat/tool/frame.tsx +0 -69
  56. package/src/components/chat/tool/index.tsx +0 -65
  57. package/src/components/chat/tool/preview.ts +0 -57
  58. package/src/components/sidebar/ContextGauge.tsx +0 -102
  59. package/src/components/sidebar/Sidebar.tsx +0 -143
  60. package/src/components/tabs/TabBar.tsx +0 -50
  61. package/src/components/ui/FileLink.tsx +0 -52
  62. package/src/config/index.ts +0 -156
  63. package/src/config/lane.ts +0 -161
  64. package/src/config/models.ts +0 -95
  65. package/src/config/rules.ts +0 -80
  66. package/src/config/schema.ts +0 -308
  67. package/src/dialogs/alert.tsx +0 -52
  68. package/src/dialogs/chafa.tsx +0 -72
  69. package/src/dialogs/confirm.tsx +0 -58
  70. package/src/dialogs/curator.tsx +0 -153
  71. package/src/dialogs/eikon-picker.tsx +0 -95
  72. package/src/dialogs/help.tsx +0 -80
  73. package/src/dialogs/history.tsx +0 -92
  74. package/src/dialogs/info.tsx +0 -115
  75. package/src/dialogs/keys.tsx +0 -170
  76. package/src/dialogs/logs.tsx +0 -42
  77. package/src/dialogs/message.tsx +0 -38
  78. package/src/dialogs/model-picker.tsx +0 -123
  79. package/src/dialogs/new-profile.tsx +0 -69
  80. package/src/dialogs/new-task.tsx +0 -103
  81. package/src/dialogs/profile.tsx +0 -55
  82. package/src/dialogs/rollback.tsx +0 -190
  83. package/src/dialogs/spawn-history.tsx +0 -80
  84. package/src/dialogs/text-prompt.tsx +0 -68
  85. package/src/dialogs/theme-picker.tsx +0 -50
  86. package/src/home/index.ts +0 -23
  87. package/src/home/store.ts +0 -267
  88. package/src/index.tsx +0 -113
  89. package/src/keys/catalog.ts +0 -115
  90. package/src/keys/chord.ts +0 -125
  91. package/src/keys/conflicts.ts +0 -48
  92. package/src/keys/context.tsx +0 -112
  93. package/src/keys/index.ts +0 -5
  94. package/src/keys/list.ts +0 -94
  95. package/src/keys/oc-compat.ts +0 -87
  96. package/src/tabs/Agents.tsx +0 -607
  97. package/src/tabs/Analytics.tsx +0 -154
  98. package/src/tabs/Chat.tsx +0 -50
  99. package/src/tabs/Config.tsx +0 -605
  100. package/src/tabs/Context.tsx +0 -599
  101. package/src/tabs/Cron.tsx +0 -294
  102. package/src/tabs/Env.tsx +0 -227
  103. package/src/tabs/Kanban.tsx +0 -367
  104. package/src/tabs/Memory.tsx +0 -294
  105. package/src/tabs/Sessions.tsx +0 -786
  106. package/src/tabs/Skills.tsx +0 -507
  107. package/src/tabs/Toolsets.tsx +0 -266
  108. package/src/theme/builtin.ts +0 -78
  109. package/src/theme/context.tsx +0 -106
  110. package/src/theme/index.ts +0 -4
  111. package/src/theme/resolve.ts +0 -134
  112. package/src/theme/syntax.ts +0 -31
  113. package/src/theme/themes/aura.json +0 -69
  114. package/src/theme/themes/ayu.json +0 -80
  115. package/src/theme/themes/carbonfox.json +0 -248
  116. package/src/theme/themes/catppuccin-frappe.json +0 -233
  117. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  118. package/src/theme/themes/catppuccin.json +0 -112
  119. package/src/theme/themes/cobalt2.json +0 -228
  120. package/src/theme/themes/cursor.json +0 -249
  121. package/src/theme/themes/dracula.json +0 -219
  122. package/src/theme/themes/everforest.json +0 -241
  123. package/src/theme/themes/flexoki.json +0 -237
  124. package/src/theme/themes/github.json +0 -233
  125. package/src/theme/themes/gruvbox.json +0 -242
  126. package/src/theme/themes/kanagawa.json +0 -77
  127. package/src/theme/themes/lucent-orng.json +0 -237
  128. package/src/theme/themes/material.json +0 -235
  129. package/src/theme/themes/matrix.json +0 -77
  130. package/src/theme/themes/mercury.json +0 -252
  131. package/src/theme/themes/monokai.json +0 -221
  132. package/src/theme/themes/nightowl.json +0 -221
  133. package/src/theme/themes/nord.json +0 -223
  134. package/src/theme/themes/one-dark.json +0 -84
  135. package/src/theme/themes/opencode.json +0 -245
  136. package/src/theme/themes/orng.json +0 -249
  137. package/src/theme/themes/osaka-jade.json +0 -93
  138. package/src/theme/themes/palenight.json +0 -222
  139. package/src/theme/themes/rosepine.json +0 -234
  140. package/src/theme/themes/solarized.json +0 -223
  141. package/src/theme/themes/synthwave84.json +0 -226
  142. package/src/theme/themes/tokyonight.json +0 -243
  143. package/src/theme/themes/vercel.json +0 -245
  144. package/src/theme/themes/vesper.json +0 -218
  145. package/src/theme/themes/zenburn.json +0 -223
  146. package/src/theme/types.ts +0 -119
  147. package/src/types/message.ts +0 -97
  148. package/src/ui/ChafaImage.tsx +0 -64
  149. package/src/ui/Splash.tsx +0 -118
  150. package/src/ui/borders.ts +0 -28
  151. package/src/ui/command.tsx +0 -104
  152. package/src/ui/dialog-select.tsx +0 -164
  153. package/src/ui/dialog.tsx +0 -102
  154. package/src/ui/fmt.ts +0 -82
  155. package/src/ui/kv.tsx +0 -28
  156. package/src/ui/shell.tsx +0 -45
  157. package/src/ui/spinner.tsx +0 -59
  158. package/src/ui/splash-art.ts +0 -123
  159. package/src/ui/table.tsx +0 -117
  160. package/src/ui/ticker.tsx +0 -90
  161. package/src/ui/toast.tsx +0 -130
  162. package/src/utils/categorical.ts +0 -77
  163. package/src/utils/chafa.ts +0 -173
  164. package/src/utils/clipboard.ts +0 -67
  165. package/src/utils/context-segments.ts +0 -317
  166. package/src/utils/control.ts +0 -495
  167. package/src/utils/drop.ts +0 -25
  168. package/src/utils/editor.ts +0 -33
  169. package/src/utils/fuzzy.ts +0 -45
  170. package/src/utils/gateway-client.ts +0 -253
  171. package/src/utils/gateway-types.ts +0 -282
  172. package/src/utils/git.ts +0 -57
  173. package/src/utils/hermes-analytics.ts +0 -134
  174. package/src/utils/hermes-home.ts +0 -821
  175. package/src/utils/hermes-kanban.ts +0 -154
  176. package/src/utils/hermes-profiles.ts +0 -217
  177. package/src/utils/interpolate.ts +0 -31
  178. package/src/utils/math-unicode.ts +0 -818
  179. package/src/utils/memory-activity.ts +0 -140
  180. package/src/utils/open-file.ts +0 -13
  181. package/src/utils/paths.ts +0 -52
  182. package/src/utils/perf.ts +0 -235
  183. package/src/utils/preferences.ts +0 -150
  184. package/src/utils/sessions-db.ts +0 -396
  185. package/src/utils/subagent-tree.ts +0 -146
  186. package/src/utils/terminal-reset.ts +0 -129
  187. package/src/utils/tips.ts +0 -67
  188. package/src/utils/tokens.ts +0 -87
@@ -1,170 +0,0 @@
1
- // /keys rebind dialog — list every catalog action grouped by scope, show
2
- // current chord + override marker + inline conflict warning. Enter opens
3
- // a TextPrompt for the chord spec (e.g. "ctrl+l", "<leader>m") — typing
4
- // the spec rather than capturing the raw keystroke sidesteps the global
5
- // useKeyboard ordering problem (useAppKeys would see the captured key
6
- // first and act on it). 'r' resets the selected row to its default.
7
-
8
- import { useState, useMemo } from "react"
9
- import { VBAR } from "../ui/table"
10
- import { useKeyboard } from "@opentui/react"
11
- import { useTheme } from "../theme"
12
- import * as prefs from "../utils/preferences"
13
- import {
14
- useKeys, DEFAULTS, conflictsWith, parse,
15
- type ActionId, type Scope, type Chord,
16
- } from "../keys"
17
- import { print as chordPrint } from "../keys/chord"
18
- import type { DialogContext } from "../ui/dialog"
19
- import { useToast } from "../ui/toast"
20
- import { openTextPrompt } from "./text-prompt"
21
- import { openConfirm } from "./confirm"
22
- import { loadOcKeybinds } from "../keys/oc-compat"
23
-
24
- type Group = { title: string; scope: Scope }
25
-
26
- const GROUPS: ReadonlyArray<Group> = [
27
- { title: "Global", scope: "global" },
28
- { title: "Composer", scope: "composer" },
29
- { title: "Lists", scope: "list" },
30
- { title: "Dialogs", scope: "dialog" },
31
- { title: "Sessions", scope: "sessions" },
32
- { title: "Agents", scope: "agents" },
33
- { title: "Config", scope: "config" },
34
- ]
35
-
36
- type Row =
37
- | { type: "header"; title: string }
38
- | { type: "action"; id: ActionId; desc: string; chord: ReadonlyArray<Chord>; override: boolean }
39
-
40
- const KeysDialog = (props: { dialog: DialogContext }) => {
41
- const theme = useTheme().theme
42
- const keys = useKeys()
43
- const toast = useToast()
44
- const overrides = prefs.get("keys") ?? {}
45
-
46
- const rows = useMemo<Row[]>(() => GROUPS.flatMap(g => {
47
- const entries = keys.all(g.scope).filter(e => e.id !== "leader")
48
- if (entries.length === 0) return []
49
- return [
50
- { type: "header" as const, title: g.title },
51
- ...entries.map(e => ({
52
- type: "action" as const,
53
- id: e.id, desc: e.desc, chord: e.chord,
54
- override: overrides[e.id] !== undefined,
55
- })),
56
- ]
57
- }), [keys, overrides])
58
-
59
- const actionRows = rows.map((r, i) => ({ r, i })).filter(x => x.r.type === "action")
60
- const [sel, setSel] = useState(0)
61
-
62
- const cur = actionRows[sel]?.r as Extract<Row, { type: "action" }> | undefined
63
- const curConflicts = cur ? conflictsWith(keys.table, cur.id) : []
64
-
65
- const write = (id: ActionId, value: string | undefined) => {
66
- const next = { ...(prefs.get("keys") ?? {}) }
67
- if (value === undefined) delete next[id]
68
- else next[id] = value
69
- prefs.set("keys", next)
70
- }
71
-
72
- const rebind = (id: ActionId) => {
73
- const now = overrides[id] ?? DEFAULTS[id].chord
74
- void openTextPrompt(props.dialog, {
75
- title: `Rebind ${id}`,
76
- label: "Chord (e.g. ctrl+k, <leader>m, shift+return; empty = unbind)",
77
- initial: now,
78
- }).then(v => {
79
- // TextPrompt's dialog.clear() replaced us; remount either way.
80
- openKeys(props.dialog)
81
- if (v === null) return
82
- const parsed = parse(v)
83
- write(id, parsed.length === 0 ? "none" : v)
84
- })
85
- }
86
-
87
- const importOc = () => {
88
- const r = loadOcKeybinds()
89
- if (r.sources.length === 0)
90
- return toast.show({ variant: "info", message: "No opencode tui.json found" })
91
- const n = Object.keys(r.overrides).length
92
- void openConfirm(props.dialog, {
93
- title: `Import ${n} keybind${n === 1 ? "" : "s"} from opencode?`,
94
- body: `${r.sources.map(s => `· ${s}`).join("\n")}\n\n${n} mapped · ${r.skipped.length} skipped (no herm equivalent)${r.skipped.length ? `:\n${r.skipped.slice(0, 8).join(", ")}${r.skipped.length > 8 ? ", …" : ""}` : ""}`,
95
- yes: "import",
96
- }).then(ok => {
97
- openKeys(props.dialog)
98
- if (!ok) return
99
- prefs.set("keys", { ...(prefs.get("keys") ?? {}), ...r.overrides })
100
- toast.show({ variant: "success",
101
- message: `Imported ${n} · skipped ${r.skipped.length}` })
102
- })
103
- }
104
-
105
- useKeyboard((key) => {
106
- if (key.name === "up") return setSel(s => Math.max(0, s - 1))
107
- if (key.name === "down") return setSel(s => Math.min(actionRows.length - 1, s + 1))
108
- if (key.name === "return" && cur) return rebind(cur.id)
109
- if (key.name === "r" && !key.ctrl && cur?.override) { write(cur.id, undefined); return }
110
- if (key.name === "o" && !key.ctrl) return importOc()
111
- })
112
-
113
- return (
114
- <box flexDirection="column" width={78}>
115
- <box height={1} flexDirection="row">
116
- <box flexGrow={1}><text fg={theme.text}><strong>Keybindings</strong></text></box>
117
- <text fg={theme.textMuted}>{`leader = ${keys.print("leader")}`}</text>
118
- </box>
119
- <box height={1} />
120
- <scrollbox scrollY maxHeight={22} verticalScrollbarOptions={VBAR}>
121
- <box flexDirection="column" width="100%">
122
- {rows.map((r, i) => {
123
- if (r.type === "header") return (
124
- <box key={`h-${r.title}`} height={1} marginTop={i > 0 ? 1 : 0}>
125
- <text fg={theme.primary}><strong>{r.title}</strong></text>
126
- </box>
127
- )
128
- const ai = actionRows.findIndex(x => x.i === i)
129
- const on = ai === sel
130
- const conf = conflictsWith(keys.table, r.id)
131
- return (
132
- <box key={r.id} height={1} flexDirection="row"
133
- backgroundColor={on ? theme.backgroundElement : undefined}
134
- onMouseOver={() => setSel(ai)}
135
- onMouseDown={() => { setSel(ai); rebind(r.id) }}>
136
- <box width={2} flexShrink={0}>
137
- <text fg={on ? theme.primary : theme.text}>{on ? "▸ " : " "}</text>
138
- </box>
139
- <box width={16} flexShrink={0} height={1} overflow="hidden">
140
- <text fg={on ? theme.accent : theme.text}>
141
- {chordPrint(r.chord, keys.print("leader")) || "—"}
142
- </text>
143
- </box>
144
- <box flexGrow={1} minWidth={0} height={1} overflow="hidden">
145
- <text fg={theme.textMuted}>{r.desc}</text>
146
- </box>
147
- <box width={5} flexShrink={0} flexDirection="row" justifyContent="flex-end">
148
- <text>
149
- {r.override ? <span fg={theme.info}>{"· "}</span> : null}
150
- {conf.length > 0 ? <span fg={theme.warning}>⚠</span> : null}
151
- </text>
152
- </box>
153
- </box>
154
- )
155
- })}
156
- </box>
157
- </scrollbox>
158
- <box height={1} />
159
- <box height={1}>
160
- {curConflicts.length > 0
161
- ? <text fg={theme.warning}>{`⚠ shares ${keys.print(cur!.id)} with: ${curConflicts.join(", ")}`}</text>
162
- : <text fg={theme.textMuted}>{`↑↓ select Enter rebind${cur?.override ? " · r reset" : ""} · o import opencode · esc close · · = overridden`}</text>}
163
- </box>
164
- </box>
165
- )
166
- }
167
-
168
- export function openKeys(dialog: DialogContext) {
169
- dialog.replace(<KeysDialog dialog={dialog} />)
170
- }
@@ -1,42 +0,0 @@
1
- // Gateway subprocess stderr tail — everything GatewayClient.log() captured
2
- // (stderr lines, protocol errors, startup-timeout markers). The ring
3
- // buffer holds the last ~200 lines regardless of what the transcript
4
- // chose to surface.
5
-
6
- import { useTheme } from "../theme"
7
- import { useGateway } from "../app/gateway"
8
- import { useDialog } from "../ui/dialog"
9
-
10
- const ERRLIKE = /error|fail|traceback|exception|\b[45]\d\d\b|refused|denied|unauthori/i
11
-
12
- const LogsDialog = () => {
13
- const theme = useTheme().theme
14
- const gw = useGateway()
15
- const lines = gw.tail(200).split("\n").filter(Boolean)
16
-
17
- return (
18
- <box flexDirection="column" width={110} height={Math.min(34, Math.max(8, lines.length + 5))}>
19
- <box height={1}><text fg={theme.primary}><strong>Gateway Logs</strong></text></box>
20
- <box height={1}><text fg={theme.textMuted}>{lines.length} lines · stderr + protocol · Esc to close</text></box>
21
- <box height={1} />
22
- {lines.length === 0 ? (
23
- <box height={1}><text fg={theme.textMuted}>No log output captured.</text></box>
24
- ) : (
25
- <scrollbox scrollY stickyScroll stickyStart="bottom" flexGrow={1}>
26
- <box flexDirection="column">
27
- {lines.map((l, i) => (
28
- <box key={i} height={1}>
29
- <text fg={ERRLIKE.test(l) ? theme.error : theme.textMuted}>
30
- {l.length > 106 ? l.slice(0, 105) + "…" : l}
31
- </text>
32
- </box>
33
- ))}
34
- </box>
35
- </scrollbox>
36
- )}
37
- </box>
38
- )
39
- }
40
-
41
- export const openLogs = (dialog: ReturnType<typeof useDialog>) =>
42
- dialog.replace(<LogsDialog />)
@@ -1,38 +0,0 @@
1
- // Per-message action menu — oc routes/session/dialog-message.tsx.
2
- // Opened by clicking a user message. Copy is local; Rewind and Fork
3
- // delegate to callbacks owned by app.tsx (they need turn state +
4
- // gateway + composer).
5
-
6
- import { DialogSelect } from "../ui/dialog-select"
7
- import type { DialogContext } from "../ui/dialog"
8
- import type { Message } from "../types/message"
9
- import { copy } from "../utils/clipboard"
10
-
11
- export type MessageOps = {
12
- rewind: (m: Message) => void
13
- fork: (m: Message) => void
14
- }
15
-
16
- export function openMessage(dialog: DialogContext, m: Message, ops: MessageOps) {
17
- const text = m.parts
18
- .filter(p => p.type === "text")
19
- .map(p => p.content)
20
- .join("")
21
-
22
- dialog.replace(
23
- <DialogSelect
24
- title="Message Actions"
25
- options={[
26
- { title: "Copy", value: "copy", description: "message text to clipboard" },
27
- { title: "Rewind here", value: "rewind", description: "undo back to this turn (destructive)" },
28
- { title: "Fork here", value: "fork", description: "branch a new session at this point" },
29
- ]}
30
- onSelect={(o) => {
31
- dialog.clear()
32
- if (o.value === "copy") return void copy(text)
33
- if (o.value === "rewind") return ops.rewind(m)
34
- if (o.value === "fork") return ops.fork(m)
35
- }}
36
- />,
37
- )
38
- }
@@ -1,123 +0,0 @@
1
- // Pick provider → model. Default scope is the *current session* (the
2
- // gateway applies the switch to the live agent when `session_id` is
3
- // passed); Tab toggles to global persist. The gateway's `config.set`
4
- // accepts a single space-separated arg string with `--provider` /
5
- // `--global` flags (same grammar as the `/model` slash command) and
6
- // routes through `_apply_model_switch`, so we send one request rather
7
- // than a provider/model pair.
8
-
9
- import { useEffect, useState, useCallback } from "react"
10
- import { useDialog } from "../ui/dialog"
11
- import { DialogSelect, type SelectOption } from "../ui/dialog-select"
12
- import { useTheme } from "../theme"
13
- import { useToast } from "../ui/toast"
14
- import type { Gateway } from "../app/gateway"
15
- import type { ConfigSetResponse, ModelOptionsResponse } from "../utils/gateway-types"
16
-
17
- type Step = "provider" | "model"
18
-
19
- type Props = {
20
- gw: Gateway
21
- /** Override the default "switch this session / global" apply. When
22
- * set, the scope toggle is hidden and the caller owns the write. */
23
- onApply?: (provider: string, model: string) => Promise<void>
24
- title?: string
25
- }
26
-
27
- const ModelPickerDialog = (props: Props) => {
28
- const dialog = useDialog()
29
- const toast = useToast()
30
- const theme = useTheme().theme
31
- const [data, setData] = useState<ModelOptionsResponse | null>(null)
32
- const [step, setStep] = useState<Step>("provider")
33
- const [provider, setProvider] = useState<string | null>(null)
34
- const [global, setGlobal] = useState(false)
35
-
36
- useEffect(() => {
37
- props.gw.request<ModelOptionsResponse>("model.options")
38
- .then(setData)
39
- .catch(() => setData({ providers: [] }))
40
- }, [props.gw])
41
-
42
- const apply = useCallback((model: string, prov: string) => {
43
- if (props.onApply) return void props.onApply(prov, model)
44
- .catch((e: Error) => toast.show({ variant: "error", message: e.message }))
45
- const value = `${model} --provider ${prov}${global ? " --global" : ""}`
46
- props.gw.request<ConfigSetResponse>("config.set", global
47
- ? { key: "model", value, session_id: undefined }
48
- : { key: "model", value })
49
- .then(r => {
50
- toast.show({ variant: "success", message: `model → ${r.value ?? model}${global ? " (global)" : ""}` })
51
- if (r.warning) toast.show({ variant: "warning", message: r.warning })
52
- })
53
- .catch((e: Error) => toast.show({ variant: "error", message: e.message }))
54
- }, [props.gw, props.onApply, global, toast])
55
-
56
- const onKey = useCallback((k: { name: string }) => {
57
- if (k.name === "tab" && !props.onApply) { setGlobal(g => !g); return true }
58
- if (k.name === "left" && step === "model") { setStep("provider"); return true }
59
- return false
60
- }, [step, props.onApply])
61
-
62
- const footer = props.onApply
63
- ? <text fg={theme.textMuted}>{step === "model" ? "←: providers" : " "}</text>
64
- : (
65
- <text fg={theme.textMuted}>
66
- <span>Scope: </span>
67
- <span fg={global ? theme.warning : theme.accent}>
68
- {global ? "global (persists to config)" : "this session"}
69
- </span>
70
- <span> · Tab: toggle{step === "model" ? " · ←: providers" : ""}</span>
71
- </text>
72
- )
73
-
74
- if (!data) return <box width={50} padding={1}><text>Loading models…</text></box>
75
-
76
- if (step === "provider") {
77
- const options: SelectOption[] = (data.providers ?? []).map(p => ({
78
- title: p.name,
79
- value: p.slug,
80
- description: p.total_models ? `${p.total_models} models` : undefined,
81
- category: p.is_current ? "Current" : "Available",
82
- }))
83
- return (
84
- <DialogSelect
85
- title={props.title ?? "Switch Provider"}
86
- options={options}
87
- current={data.provider}
88
- onSelect={(o) => { setProvider(o.value); setStep("model") }}
89
- onKey={onKey}
90
- placeholder="Search providers..."
91
- footer={footer}
92
- />
93
- )
94
- }
95
-
96
- const p = data.providers?.find(pp => pp.slug === provider)
97
- const options: SelectOption[] = (p?.models ?? []).map(m => ({
98
- title: m,
99
- value: m,
100
- }))
101
-
102
- return (
103
- <DialogSelect
104
- title={props.title ? `${props.title} · ${p?.name ?? provider}` : `Switch Model (${p?.name ?? provider})`}
105
- options={options}
106
- current={data.model}
107
- onSelect={(o) => {
108
- if (provider) apply(o.value, provider)
109
- dialog.clear()
110
- }}
111
- onKey={onKey}
112
- placeholder="Search models..."
113
- footer={footer}
114
- />
115
- )
116
- }
117
-
118
- export const openModelPicker = (
119
- dialog: ReturnType<typeof useDialog>, gw: Gateway,
120
- opts?: { title?: string; onApply?: (provider: string, model: string) => Promise<void> },
121
- ) => {
122
- dialog.replace(<ModelPickerDialog gw={gw} title={opts?.title} onApply={opts?.onApply} />)
123
- }
@@ -1,69 +0,0 @@
1
- import { useState } from "react"
2
- import { useKeyboard } from "@opentui/react"
3
- import { useTheme } from "../theme"
4
- import { validateName } from "../utils/hermes-profiles"
5
- import type { DialogContext } from "../ui/dialog"
6
-
7
- type Result = { name: string; cloneFrom: string | null; alias: boolean }
8
-
9
- export function openCreateProfile(dialog: DialogContext, opts: { existing: string[] }): Promise<Result | null> {
10
- return new Promise(resolve => {
11
- const done = (r: Result | null) => { dialog.clear(); resolve(r) }
12
- dialog.replace(<Form existing={opts.existing} done={done} />)
13
- })
14
- }
15
-
16
- const Form = ({ existing, done }: { existing: string[]; done: (r: Result | null) => void }) => {
17
- const theme = useTheme().theme
18
- const [name, setName] = useState("")
19
- const [cloneIdx, setCloneIdx] = useState(0)
20
- const [alias, setAlias] = useState(true)
21
- const options = ["(fresh)", ...existing]
22
- const err = name ? validateName(name, existing) : null
23
- const valid = !!name && !err
24
-
25
- useKeyboard((key) => {
26
- if (key.name === "escape") return done(null)
27
- if (key.name === "return") {
28
- if (!valid) return
29
- return done({ name, cloneFrom: cloneIdx === 0 ? null : options[cloneIdx], alias })
30
- }
31
- if (key.name === "up") return setCloneIdx(i => Math.max(0, i - 1))
32
- if (key.name === "down") return setCloneIdx(i => Math.min(options.length - 1, i + 1))
33
- if (key.name === "tab") return setAlias(a => !a)
34
- if (key.name === "backspace") return setName(n => n.slice(0, -1))
35
- if (key.raw && key.raw.length === 1 && /[a-z0-9_-]/.test(key.raw))
36
- return setName(n => n + key.raw)
37
- })
38
-
39
- return (
40
- <box flexDirection="column" width={54}>
41
- <box height={1}><text fg={theme.primary}><strong>New Profile</strong></text></box>
42
- <box height={1} />
43
- <box height={1} flexDirection="row">
44
- <box width={11}><text fg={theme.textMuted}>Name</text></box>
45
- <text>
46
- <span fg={valid || !name ? theme.text : theme.error}>{name}</span>
47
- <span fg={theme.accent}>█</span>
48
- </text>
49
- </box>
50
- <box height={1}><text fg={theme.textMuted}> a-z 0-9 _ - · lowercase</text></box>
51
- <box height={1} />
52
- <box height={1}><text fg={theme.textMuted}>Clone from (↑↓)</text></box>
53
- {options.map((o, i) => (
54
- <box key={o} height={1}>
55
- <text fg={i === cloneIdx ? theme.accent : theme.text}>
56
- {i === cloneIdx ? "▸ " : " "}{o}
57
- </text>
58
- </box>
59
- ))}
60
- <box height={1} />
61
- <box height={1}><text fg={theme.textMuted}>
62
- {`[Tab] shell alias: ${alias ? "yes" : "no"}`}
63
- </text></box>
64
- <box height={1}><text fg={theme.textMuted}>
65
- {valid ? "Enter create · Esc cancel" : err ?? "type a name"}
66
- </text></box>
67
- </box>
68
- )
69
- }
@@ -1,103 +0,0 @@
1
- // Create/edit a kanban task. Tab cycles fields; ↑↓ pick assignee or
2
- // bump priority depending on focused field; Enter submits when title
3
- // is non-empty. Body is single-line here — longer specs go in as a
4
- // follow-up comment (c) from the board.
5
-
6
- import { useState } from "react"
7
- import { useKeyboard } from "@opentui/react"
8
- import { useTheme } from "../theme"
9
- import type { DialogContext } from "../ui/dialog"
10
-
11
- export type Draft = {
12
- title: string; body: string; assignee: string | null
13
- priority: number; parent: string | null
14
- }
15
-
16
- type Field = "title" | "body" | "assignee" | "priority"
17
- const ORDER: Field[] = ["title", "body", "assignee", "priority"]
18
-
19
- export function openCreateTask(
20
- dialog: DialogContext,
21
- opts: { assignees: string[]; parent?: { id: string; title: string } },
22
- ): Promise<Draft | null> {
23
- return new Promise(resolve => {
24
- const done = (r: Draft | null) => { dialog.clear(); resolve(r) }
25
- dialog.replace(<Form pool={opts.assignees} parent={opts.parent} done={done} />)
26
- })
27
- }
28
-
29
- const Form = (p: {
30
- pool: string[]; parent?: { id: string; title: string }; done: (r: Draft | null) => void
31
- }) => {
32
- const theme = useTheme().theme
33
- const pool = ["(unassigned)", ...p.pool]
34
- const [field, setField] = useState<Field>("title")
35
- const [title, setTitle] = useState("")
36
- const [body, setBody] = useState("")
37
- const [who, setWho] = useState(0)
38
- const [pri, setPri] = useState(0)
39
- const valid = title.trim().length > 0
40
-
41
- const edit = (fn: (s: string) => string) =>
42
- field === "title" ? setTitle(fn) : setBody(fn)
43
-
44
- useKeyboard((key) => {
45
- if (key.name === "escape") return p.done(null)
46
- if (key.name === "return") {
47
- if (!valid) return
48
- return p.done({
49
- title: title.trim(), body: body.trim(),
50
- assignee: who === 0 ? null : pool[who],
51
- priority: pri, parent: p.parent?.id ?? null,
52
- })
53
- }
54
- if (key.name === "tab") {
55
- const i = ORDER.indexOf(field)
56
- return setField(ORDER[(i + (key.shift ? ORDER.length - 1 : 1)) % ORDER.length])
57
- }
58
- if (key.name === "up" || key.name === "down") {
59
- const d = key.name === "up" ? -1 : 1
60
- if (field === "priority") return setPri(n => Math.max(0, Math.min(9, n + d)))
61
- return setWho(i => (i + d + pool.length) % pool.length)
62
- }
63
- if (field === "title" || field === "body") {
64
- if (key.name === "backspace") return edit(s => s.slice(0, -1))
65
- if (key.ctrl && key.name === "u") return edit(() => "")
66
- if (!key.ctrl && !key.meta && key.raw && key.raw.length === 1 && key.raw >= " ")
67
- return edit(s => s + key.raw)
68
- }
69
- })
70
-
71
- const row = (f: Field, label: string, val: string) => (
72
- <box height={1} flexDirection="row">
73
- <box width={11}><text fg={field === f ? theme.accent : theme.textMuted}>
74
- {field === f ? "▸ " : " "}{label}
75
- </text></box>
76
- <box flexGrow={1} minWidth={0} height={1} overflow="hidden">
77
- <text>
78
- <span fg={theme.text}>{val}</span>
79
- {field === f && (f === "title" || f === "body")
80
- ? <span fg={theme.accent}>█</span> : null}
81
- </text>
82
- </box>
83
- </box>
84
- )
85
-
86
- return (
87
- <box flexDirection="column" width={64}>
88
- <box height={1}><text fg={theme.primary}>
89
- <strong>{p.parent ? `New Task · child of ${p.parent.id}` : "New Task"}</strong>
90
- </text></box>
91
- {p.parent ? <box height={1}><text fg={theme.textMuted}> {p.parent.title}</text></box> : null}
92
- <box height={1} />
93
- {row("title", "Title", title)}
94
- {row("body", "Body", body || "—")}
95
- {row("assignee", "Assignee", pool[who])}
96
- {row("priority", "Priority", pri ? `P${pri}` : "—")}
97
- <box height={1} />
98
- <box height={1}><text fg={theme.textMuted}>
99
- {valid ? "Enter create · Tab field · ↑↓ pick · Esc cancel" : "type a title · Tab field"}
100
- </text></box>
101
- </box>
102
- )
103
- }
@@ -1,55 +0,0 @@
1
- // Per-profile action menu. All mutations route through the hermes CLI
2
- // via `shell.exec` so herm doesn't duplicate validation/cleanup logic.
3
- // "Open …" actions use the OS handler (openFile) rather than an
4
- // in-TUI editor — SOUL.md and config.yaml are multi-hundred-line
5
- // files, not composer-sized inputs.
6
-
7
- import { DialogSelect, type SelectOption } from "../ui/dialog-select"
8
- import type { DialogContext } from "../ui/dialog"
9
- import type { ProfileInfo } from "../utils/hermes-profiles"
10
- import { openFile } from "../utils/open-file"
11
-
12
- export type ProfileOps = {
13
- sticky: (p: ProfileInfo) => void
14
- unsticky: () => void
15
- export: (p: ProfileInfo) => void
16
- remove: (p: ProfileInfo) => void
17
- }
18
-
19
- export function openProfileMenu(dialog: DialogContext, p: ProfileInfo, ops: ProfileOps) {
20
- const opts: SelectOption[] = [
21
- { category: "Open", value: "soul", title: "SOUL.md", description: "edit persona/system prompt" },
22
- { category: "Open", value: "config", title: "config.yaml", description: "model, provider, toolsets" },
23
- ...(p.has_env
24
- ? [{ category: "Open", value: "env", title: ".env", description: "API keys + secrets" }] : []),
25
- { category: "Open", value: "dir", title: "Directory", description: p.path },
26
- ...(p.is_sticky
27
- ? [{ category: "Default", value: "unsticky", title: "Clear sticky default",
28
- description: "hermes profile use --clear" }]
29
- : [{ category: "Default", value: "sticky", title: "Set as sticky default",
30
- description: `hermes profile use ${p.name}` }]),
31
- { category: "Manage", value: "export", title: "Export",
32
- description: `hermes profile export ${p.name}` },
33
- ...(p.is_default || p.is_active ? []
34
- : [{ category: "Manage", value: "delete", title: "Delete",
35
- description: "irreversible — removes config, env, memory, sessions" }]),
36
- ]
37
-
38
- dialog.replace(
39
- <DialogSelect
40
- title={`Profile · ${p.name}${p.is_active ? " (active)" : ""}`}
41
- options={opts}
42
- onSelect={(o) => {
43
- dialog.clear()
44
- if (o.value === "soul") return openFile(p.sources.soul.file)
45
- if (o.value === "config") return openFile(p.sources.config.file)
46
- if (o.value === "env") return openFile(p.sources.env.file)
47
- if (o.value === "dir") return openFile(p.path)
48
- if (o.value === "sticky") return ops.sticky(p)
49
- if (o.value === "unsticky") return ops.unsticky()
50
- if (o.value === "export") return ops.export(p)
51
- if (o.value === "delete") return ops.remove(p)
52
- }}
53
- />,
54
- )
55
- }