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

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 (192) hide show
  1. package/README.md +8 -4
  2. package/assets/eikons/ares.eikon +367 -0
  3. package/assets/eikons/default.eikon +398 -0
  4. package/assets/eikons/mono.eikon +395 -0
  5. package/db.worker.js +81 -0
  6. package/highlights-eq9cgrbb.scm +604 -0
  7. package/highlights-ghv9g403.scm +205 -0
  8. package/highlights-hk7bwhj4.scm +284 -0
  9. package/highlights-r812a2qc.scm +150 -0
  10. package/highlights-x6tmsnaa.scm +115 -0
  11. package/index.js +4151 -0
  12. package/injections-73j83es3.scm +27 -0
  13. package/package.json +14 -64
  14. package/parser.worker.js +8 -0
  15. package/tree-sitter-3jzf13jk.wasm +0 -0
  16. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  17. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  18. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  19. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  20. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  21. package/scripts/postinstall.ts +0 -29
  22. package/src/app/gateway.tsx +0 -83
  23. package/src/app/gatewayEvents.ts +0 -203
  24. package/src/app/launch.ts +0 -41
  25. package/src/app/skin.tsx +0 -31
  26. package/src/app/spawnHistory.ts +0 -75
  27. package/src/app/tabs.ts +0 -23
  28. package/src/app/turnReducer.ts +0 -390
  29. package/src/app/useAppKeys.ts +0 -268
  30. package/src/app/useAtRefPopover.ts +0 -99
  31. package/src/app/useInputHistory.ts +0 -66
  32. package/src/app/useSession.ts +0 -102
  33. package/src/app/useSlashCommands.ts +0 -70
  34. package/src/app/useSlashPopover.ts +0 -48
  35. package/src/app.tsx +0 -917
  36. package/src/commands/slash.ts +0 -151
  37. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  38. package/src/components/avatar/eikon.ts +0 -144
  39. package/src/components/avatar/states/error.ts +0 -1155
  40. package/src/components/avatar/states/idle.ts +0 -1155
  41. package/src/components/avatar/states/index.ts +0 -30
  42. package/src/components/avatar/states/listening.ts +0 -1155
  43. package/src/components/avatar/states/speaking.ts +0 -1155
  44. package/src/components/avatar/states/thinking.ts +0 -1155
  45. package/src/components/avatar/states/working.ts +0 -1155
  46. package/src/components/chat/AtRefPopover.tsx +0 -54
  47. package/src/components/chat/CodeBlock.tsx +0 -67
  48. package/src/components/chat/Composer.tsx +0 -347
  49. package/src/components/chat/DiffBlock.tsx +0 -116
  50. package/src/components/chat/ErrorBlock.tsx +0 -70
  51. package/src/components/chat/MediaChip.tsx +0 -114
  52. package/src/components/chat/MessageItem.tsx +0 -282
  53. package/src/components/chat/MessageList.tsx +0 -114
  54. package/src/components/chat/PromptCard.tsx +0 -359
  55. package/src/components/chat/SlashPopover.tsx +0 -158
  56. package/src/components/chat/ThoughtCloud.tsx +0 -185
  57. package/src/components/chat/TypingIndicator.tsx +0 -25
  58. package/src/components/chat/tool/Subagent.tsx +0 -75
  59. package/src/components/chat/tool/frame.tsx +0 -69
  60. package/src/components/chat/tool/index.tsx +0 -65
  61. package/src/components/chat/tool/preview.ts +0 -57
  62. package/src/components/sidebar/ContextGauge.tsx +0 -102
  63. package/src/components/sidebar/Sidebar.tsx +0 -143
  64. package/src/components/tabs/TabBar.tsx +0 -50
  65. package/src/components/ui/FileLink.tsx +0 -52
  66. package/src/config/index.ts +0 -156
  67. package/src/config/lane.ts +0 -161
  68. package/src/config/models.ts +0 -95
  69. package/src/config/rules.ts +0 -80
  70. package/src/config/schema.ts +0 -308
  71. package/src/dialogs/alert.tsx +0 -52
  72. package/src/dialogs/chafa.tsx +0 -72
  73. package/src/dialogs/confirm.tsx +0 -58
  74. package/src/dialogs/curator.tsx +0 -153
  75. package/src/dialogs/eikon-picker.tsx +0 -95
  76. package/src/dialogs/help.tsx +0 -80
  77. package/src/dialogs/history.tsx +0 -92
  78. package/src/dialogs/info.tsx +0 -115
  79. package/src/dialogs/keys.tsx +0 -170
  80. package/src/dialogs/logs.tsx +0 -42
  81. package/src/dialogs/message.tsx +0 -38
  82. package/src/dialogs/model-picker.tsx +0 -123
  83. package/src/dialogs/new-profile.tsx +0 -69
  84. package/src/dialogs/new-task.tsx +0 -103
  85. package/src/dialogs/profile.tsx +0 -55
  86. package/src/dialogs/rollback.tsx +0 -190
  87. package/src/dialogs/spawn-history.tsx +0 -80
  88. package/src/dialogs/text-prompt.tsx +0 -68
  89. package/src/dialogs/theme-picker.tsx +0 -50
  90. package/src/home/index.ts +0 -23
  91. package/src/home/store.ts +0 -267
  92. package/src/index.tsx +0 -113
  93. package/src/keys/catalog.ts +0 -115
  94. package/src/keys/chord.ts +0 -125
  95. package/src/keys/conflicts.ts +0 -48
  96. package/src/keys/context.tsx +0 -112
  97. package/src/keys/index.ts +0 -5
  98. package/src/keys/list.ts +0 -94
  99. package/src/keys/oc-compat.ts +0 -87
  100. package/src/tabs/Agents.tsx +0 -607
  101. package/src/tabs/Analytics.tsx +0 -154
  102. package/src/tabs/Chat.tsx +0 -50
  103. package/src/tabs/Config.tsx +0 -605
  104. package/src/tabs/Context.tsx +0 -599
  105. package/src/tabs/Cron.tsx +0 -294
  106. package/src/tabs/Env.tsx +0 -227
  107. package/src/tabs/Kanban.tsx +0 -367
  108. package/src/tabs/Memory.tsx +0 -294
  109. package/src/tabs/Sessions.tsx +0 -786
  110. package/src/tabs/Skills.tsx +0 -507
  111. package/src/tabs/Toolsets.tsx +0 -266
  112. package/src/theme/builtin.ts +0 -78
  113. package/src/theme/context.tsx +0 -106
  114. package/src/theme/index.ts +0 -4
  115. package/src/theme/resolve.ts +0 -134
  116. package/src/theme/syntax.ts +0 -31
  117. package/src/theme/themes/aura.json +0 -69
  118. package/src/theme/themes/ayu.json +0 -80
  119. package/src/theme/themes/carbonfox.json +0 -248
  120. package/src/theme/themes/catppuccin-frappe.json +0 -233
  121. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  122. package/src/theme/themes/catppuccin.json +0 -112
  123. package/src/theme/themes/cobalt2.json +0 -228
  124. package/src/theme/themes/cursor.json +0 -249
  125. package/src/theme/themes/dracula.json +0 -219
  126. package/src/theme/themes/everforest.json +0 -241
  127. package/src/theme/themes/flexoki.json +0 -237
  128. package/src/theme/themes/github.json +0 -233
  129. package/src/theme/themes/gruvbox.json +0 -242
  130. package/src/theme/themes/kanagawa.json +0 -77
  131. package/src/theme/themes/lucent-orng.json +0 -237
  132. package/src/theme/themes/material.json +0 -235
  133. package/src/theme/themes/matrix.json +0 -77
  134. package/src/theme/themes/mercury.json +0 -252
  135. package/src/theme/themes/monokai.json +0 -221
  136. package/src/theme/themes/nightowl.json +0 -221
  137. package/src/theme/themes/nord.json +0 -223
  138. package/src/theme/themes/one-dark.json +0 -84
  139. package/src/theme/themes/opencode.json +0 -245
  140. package/src/theme/themes/orng.json +0 -249
  141. package/src/theme/themes/osaka-jade.json +0 -93
  142. package/src/theme/themes/palenight.json +0 -222
  143. package/src/theme/themes/rosepine.json +0 -234
  144. package/src/theme/themes/solarized.json +0 -223
  145. package/src/theme/themes/synthwave84.json +0 -226
  146. package/src/theme/themes/tokyonight.json +0 -243
  147. package/src/theme/themes/vercel.json +0 -245
  148. package/src/theme/themes/vesper.json +0 -218
  149. package/src/theme/themes/zenburn.json +0 -223
  150. package/src/theme/types.ts +0 -119
  151. package/src/types/message.ts +0 -97
  152. package/src/ui/ChafaImage.tsx +0 -64
  153. package/src/ui/Splash.tsx +0 -118
  154. package/src/ui/borders.ts +0 -28
  155. package/src/ui/command.tsx +0 -104
  156. package/src/ui/dialog-select.tsx +0 -164
  157. package/src/ui/dialog.tsx +0 -102
  158. package/src/ui/fmt.ts +0 -82
  159. package/src/ui/kv.tsx +0 -28
  160. package/src/ui/shell.tsx +0 -45
  161. package/src/ui/spinner.tsx +0 -59
  162. package/src/ui/splash-art.ts +0 -123
  163. package/src/ui/table.tsx +0 -117
  164. package/src/ui/ticker.tsx +0 -90
  165. package/src/ui/toast.tsx +0 -130
  166. package/src/utils/categorical.ts +0 -77
  167. package/src/utils/chafa.ts +0 -173
  168. package/src/utils/clipboard.ts +0 -67
  169. package/src/utils/context-segments.ts +0 -317
  170. package/src/utils/control.ts +0 -495
  171. package/src/utils/drop.ts +0 -25
  172. package/src/utils/editor.ts +0 -33
  173. package/src/utils/fuzzy.ts +0 -45
  174. package/src/utils/gateway-client.ts +0 -253
  175. package/src/utils/gateway-types.ts +0 -282
  176. package/src/utils/git.ts +0 -57
  177. package/src/utils/hermes-analytics.ts +0 -134
  178. package/src/utils/hermes-home.ts +0 -821
  179. package/src/utils/hermes-kanban.ts +0 -154
  180. package/src/utils/hermes-profiles.ts +0 -217
  181. package/src/utils/interpolate.ts +0 -31
  182. package/src/utils/math-unicode.ts +0 -818
  183. package/src/utils/memory-activity.ts +0 -140
  184. package/src/utils/open-file.ts +0 -13
  185. package/src/utils/paths.ts +0 -52
  186. package/src/utils/perf.ts +0 -235
  187. package/src/utils/preferences.ts +0 -150
  188. package/src/utils/sessions-db.ts +0 -396
  189. package/src/utils/subagent-tree.ts +0 -146
  190. package/src/utils/terminal-reset.ts +0 -129
  191. package/src/utils/tips.ts +0 -67
  192. 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
- }