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

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