@agentprojectcontext/apx 1.15.6 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,60 @@
1
+ import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/tui"
2
+ import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
3
+ import { isRecord } from "@/util/record"
4
+
5
+ type RuntimeSlotMap = TuiSlotMap<Record<string, object>>
6
+
7
+ type Slot = <Name extends string>(props: TuiSlotProps<Name>) => JSX.Element | null
8
+ export type HostSlotPlugin<Slots extends Record<string, object> = {}> = SolidPlugin<TuiSlotMap<Slots>, TuiSlotContext>
9
+
10
+ export type HostPluginApi = TuiPluginApi
11
+ export type HostSlots = {
12
+ register: {
13
+ (plugin: HostSlotPlugin): () => void
14
+ <Slots extends Record<string, object>>(plugin: HostSlotPlugin<Slots>): () => void
15
+ }
16
+ }
17
+
18
+ function empty<Name extends string>(_props: TuiSlotProps<Name>) {
19
+ return null
20
+ }
21
+
22
+ let view: Slot = empty
23
+
24
+ export const Slot: Slot = (props) => view(props)
25
+
26
+ function isHostSlotPlugin(value: unknown): value is HostSlotPlugin<Record<string, object>> {
27
+ if (!isRecord(value)) return false
28
+ if (typeof value.id !== "string") return false
29
+ if (!isRecord(value.slots)) return false
30
+ return true
31
+ }
32
+
33
+ export function setupSlots(api: HostPluginApi): HostSlots {
34
+ const reg = createSolidSlotRegistry<RuntimeSlotMap, TuiSlotContext>(
35
+ api.renderer,
36
+ {
37
+ theme: api.theme,
38
+ },
39
+ {
40
+ onPluginError(event) {
41
+ console.error("[tui.slot] plugin error", {
42
+ plugin: event.pluginId,
43
+ slot: event.slot,
44
+ phase: event.phase,
45
+ source: event.source,
46
+ message: event.error.message,
47
+ })
48
+ },
49
+ },
50
+ )
51
+
52
+ const slot = createSlot<RuntimeSlotMap, TuiSlotContext>(reg)
53
+ view = (props) => slot(props)
54
+ return {
55
+ register(plugin: HostSlotPlugin) {
56
+ if (!isHostSlotPlugin(plugin)) return () => {}
57
+ return reg.register(plugin)
58
+ },
59
+ }
60
+ }
@@ -0,0 +1,96 @@
1
+ import { Prompt, type PromptRef } from "@tui/component/prompt"
2
+ import { createEffect, createSignal, onMount } from "solid-js"
3
+ import { Logo } from "../component/logo"
4
+ import { useProject } from "../context/project"
5
+ import { useSync } from "../context/sync"
6
+ import { Toast } from "../ui/toast"
7
+ import { useArgs } from "../context/args"
8
+ import { useRouteData } from "@tui/context/route"
9
+ import { usePromptRef } from "../context/prompt"
10
+ import { useLocal } from "../context/local"
11
+ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
12
+ import { useEditorContext } from "@tui/context/editor"
13
+
14
+ let once = false
15
+ const placeholder = {
16
+ normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
17
+ shell: ["ls -la", "git status", "pwd"],
18
+ }
19
+
20
+ export function Home() {
21
+ const sync = useSync()
22
+ const project = useProject()
23
+ const route = useRouteData("home")
24
+ const promptRef = usePromptRef()
25
+ const [ref, setRef] = createSignal<PromptRef | undefined>()
26
+ const args = useArgs()
27
+ const local = useLocal()
28
+ const editor = useEditorContext()
29
+ let sent = false
30
+
31
+ onMount(() => {
32
+ editor.clearSelection()
33
+ })
34
+
35
+ const bind = (r: PromptRef | undefined) => {
36
+ setRef(r)
37
+ promptRef.set(r)
38
+ if (once || !r) return
39
+ if (route.prompt) {
40
+ r.set(route.prompt)
41
+ once = true
42
+ return
43
+ }
44
+ if (!args.prompt) return
45
+ r.set({ input: args.prompt, parts: [] })
46
+ once = true
47
+ }
48
+
49
+ // Wait for sync and model store to be ready before auto-submitting --prompt
50
+ createEffect(() => {
51
+ const r = ref()
52
+ if (sent) return
53
+ if (!r) return
54
+ if (!sync.ready || !local.model.ready) return
55
+ if (!args.prompt) return
56
+ if (r.current.input !== args.prompt) return
57
+ sent = true
58
+ r.submit()
59
+ })
60
+
61
+ return (
62
+ <>
63
+ <box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
64
+ <box flexGrow={1} minHeight={0} />
65
+ <box height={4} minHeight={0} flexShrink={1} />
66
+ <box flexShrink={0}>
67
+ <TuiPluginRuntime.Slot name="home_logo" mode="replace">
68
+ <Logo />
69
+ </TuiPluginRuntime.Slot>
70
+ </box>
71
+ <box height={1} minHeight={0} flexShrink={1} />
72
+ <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
73
+ <TuiPluginRuntime.Slot
74
+ name="home_prompt"
75
+ mode="replace"
76
+ workspace_id={project.workspace.current()}
77
+ ref={bind}
78
+ >
79
+ <Prompt
80
+ ref={bind}
81
+ workspaceID={project.workspace.current()}
82
+ right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
83
+ placeholders={placeholder}
84
+ />
85
+ </TuiPluginRuntime.Slot>
86
+ </box>
87
+ <TuiPluginRuntime.Slot name="home_bottom" />
88
+ <box flexGrow={1} minHeight={0} />
89
+ <Toast />
90
+ </box>
91
+ <box width="100%" flexShrink={0}>
92
+ <TuiPluginRuntime.Slot name="home_footer" mode="single_winner" />
93
+ </box>
94
+ </>
95
+ )
96
+ }
@@ -0,0 +1,76 @@
1
+ import { createMemo, onMount } from "solid-js"
2
+ import { useSync } from "@tui/context/sync"
3
+ import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
4
+ import type { TextPart } from "@opencode-ai/sdk/v2"
5
+ import { Locale } from "@/util/locale"
6
+ import { useSDK } from "@tui/context/sdk"
7
+ import { useRoute } from "@tui/context/route"
8
+ import { useDialog, type DialogContext } from "../../ui/dialog"
9
+ import type { PromptInfo } from "@tui/component/prompt/history"
10
+ import { strip } from "@tui/component/prompt/part"
11
+
12
+ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
13
+ const sync = useSync()
14
+ const dialog = useDialog()
15
+ const sdk = useSDK()
16
+ const route = useRoute()
17
+
18
+ onMount(() => {
19
+ dialog.setSize("large")
20
+ })
21
+
22
+ const options = createMemo((): DialogSelectOption<string | undefined>[] => {
23
+ const messages = sync.data.message[props.sessionID] ?? []
24
+ const fullSession = {
25
+ title: "Full session",
26
+ value: undefined,
27
+ onSelect: async (dialog: DialogContext) => {
28
+ const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
29
+ route.navigate({
30
+ sessionID: forked.data!.id,
31
+ type: "session",
32
+ })
33
+ dialog.clear()
34
+ },
35
+ } satisfies DialogSelectOption<string | undefined>
36
+ const result = [] as DialogSelectOption<string | undefined>[]
37
+ for (const message of messages) {
38
+ if (message.role !== "user") continue
39
+ const part = (sync.data.part[message.id] ?? []).find(
40
+ (x) => x.type === "text" && !x.synthetic && !x.ignored,
41
+ ) as TextPart
42
+ if (!part) continue
43
+ result.push({
44
+ title: part.text.replace(/\n/g, " "),
45
+ value: message.id,
46
+ footer: Locale.time(message.time.created),
47
+ onSelect: async (dialog) => {
48
+ const forked = await sdk.client.session.fork({
49
+ sessionID: props.sessionID,
50
+ messageID: message.id,
51
+ })
52
+ const parts = sync.data.part[message.id] ?? []
53
+ const prompt = parts.reduce(
54
+ (agg, part) => {
55
+ if (part.type === "text") {
56
+ if (!part.synthetic) agg.input += part.text
57
+ }
58
+ if (part.type === "file") agg.parts.push(strip(part))
59
+ return agg
60
+ },
61
+ { input: "", parts: [] as PromptInfo["parts"] },
62
+ )
63
+ route.navigate({
64
+ sessionID: forked.data!.id,
65
+ type: "session",
66
+ prompt,
67
+ })
68
+ dialog.clear()
69
+ },
70
+ })
71
+ }
72
+ return [fullSession, ...result.reverse()]
73
+ })
74
+
75
+ return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
76
+ }
@@ -0,0 +1,108 @@
1
+ import { createMemo } from "solid-js"
2
+ import { useSync } from "@tui/context/sync"
3
+ import { DialogSelect } from "@tui/ui/dialog-select"
4
+ import { useSDK } from "@tui/context/sdk"
5
+ import { useRoute } from "@tui/context/route"
6
+ import * as Clipboard from "@tui/util/clipboard"
7
+ import type { PromptInfo } from "@tui/component/prompt/history"
8
+ import { strip } from "@tui/component/prompt/part"
9
+
10
+ export function DialogMessage(props: {
11
+ messageID: string
12
+ sessionID: string
13
+ setPrompt?: (prompt: PromptInfo) => void
14
+ }) {
15
+ const sync = useSync()
16
+ const sdk = useSDK()
17
+ const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
18
+ const route = useRoute()
19
+
20
+ return (
21
+ <DialogSelect
22
+ title="Message Actions"
23
+ options={[
24
+ {
25
+ title: "Revert",
26
+ value: "session.revert",
27
+ description: "undo messages and file changes",
28
+ onSelect: (dialog) => {
29
+ const msg = message()
30
+ if (!msg) return
31
+
32
+ void sdk.client.session.revert({
33
+ sessionID: props.sessionID,
34
+ messageID: msg.id,
35
+ })
36
+
37
+ if (props.setPrompt) {
38
+ const parts = sync.data.part[msg.id]
39
+ const promptInfo = parts.reduce(
40
+ (agg, part) => {
41
+ if (part.type === "text") {
42
+ if (!part.synthetic) agg.input += part.text
43
+ }
44
+ if (part.type === "file") agg.parts.push(strip(part))
45
+ return agg
46
+ },
47
+ { input: "", parts: [] as PromptInfo["parts"] },
48
+ )
49
+ props.setPrompt(promptInfo)
50
+ }
51
+
52
+ dialog.clear()
53
+ },
54
+ },
55
+ {
56
+ title: "Copy",
57
+ value: "message.copy",
58
+ description: "message text to clipboard",
59
+ onSelect: async (dialog) => {
60
+ const msg = message()
61
+ if (!msg) return
62
+
63
+ const parts = sync.data.part[msg.id]
64
+ const text = parts.reduce((agg, part) => {
65
+ if (part.type === "text" && !part.synthetic) {
66
+ agg += part.text
67
+ }
68
+ return agg
69
+ }, "")
70
+
71
+ await Clipboard.copy(text)
72
+ dialog.clear()
73
+ },
74
+ },
75
+ {
76
+ title: "Fork",
77
+ value: "session.fork",
78
+ description: "create a new session",
79
+ onSelect: async (dialog) => {
80
+ const result = await sdk.client.session.fork({
81
+ sessionID: props.sessionID,
82
+ messageID: props.messageID,
83
+ })
84
+ const msg = message()
85
+ const prompt = msg
86
+ ? sync.data.part[msg.id].reduce(
87
+ (agg, part) => {
88
+ if (part.type === "text") {
89
+ if (!part.synthetic) agg.input += part.text
90
+ }
91
+ if (part.type === "file") agg.parts.push(part)
92
+ return agg
93
+ },
94
+ { input: "", parts: [] as PromptInfo["parts"] },
95
+ )
96
+ : undefined
97
+ route.navigate({
98
+ sessionID: result.data!.id,
99
+ type: "session",
100
+ prompt,
101
+ })
102
+ dialog.clear()
103
+ },
104
+ },
105
+ ]}
106
+ />
107
+ )
108
+ }
@@ -0,0 +1,26 @@
1
+ import { DialogSelect } from "@tui/ui/dialog-select"
2
+ import { useRoute } from "@tui/context/route"
3
+
4
+ export function DialogSubagent(props: { sessionID: string }) {
5
+ const route = useRoute()
6
+
7
+ return (
8
+ <DialogSelect
9
+ title="Subagent Actions"
10
+ options={[
11
+ {
12
+ title: "Open",
13
+ value: "subagent.view",
14
+ description: "the subagent's session",
15
+ onSelect: (dialog) => {
16
+ route.navigate({
17
+ type: "session",
18
+ sessionID: props.sessionID,
19
+ })
20
+ dialog.clear()
21
+ },
22
+ },
23
+ ]}
24
+ />
25
+ )
26
+ }
@@ -0,0 +1,47 @@
1
+ import { createMemo, onMount } from "solid-js"
2
+ import { useSync } from "@tui/context/sync"
3
+ import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
4
+ import type { TextPart } from "@opencode-ai/sdk/v2"
5
+ import { Locale } from "@/util/locale"
6
+ import { DialogMessage } from "./dialog-message"
7
+ import { useDialog } from "../../ui/dialog"
8
+ import type { PromptInfo } from "../../component/prompt/history"
9
+
10
+ export function DialogTimeline(props: {
11
+ sessionID: string
12
+ onMove: (messageID: string) => void
13
+ setPrompt?: (prompt: PromptInfo) => void
14
+ }) {
15
+ const sync = useSync()
16
+ const dialog = useDialog()
17
+
18
+ onMount(() => {
19
+ dialog.setSize("large")
20
+ })
21
+
22
+ const options = createMemo((): DialogSelectOption<string>[] => {
23
+ const messages = sync.data.message[props.sessionID] ?? []
24
+ const result = [] as DialogSelectOption<string>[]
25
+ for (const message of messages) {
26
+ if (message.role !== "user") continue
27
+ const part = (sync.data.part[message.id] ?? []).find(
28
+ (x) => x.type === "text" && !x.synthetic && !x.ignored,
29
+ ) as TextPart
30
+ if (!part) continue
31
+ result.push({
32
+ title: part.text.replace(/\n/g, " "),
33
+ value: message.id,
34
+ footer: Locale.time(message.time.created),
35
+ onSelect: (dialog) => {
36
+ dialog.replace(() => (
37
+ <DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
38
+ ))
39
+ },
40
+ })
41
+ }
42
+ result.reverse()
43
+ return result
44
+ })
45
+
46
+ return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Timeline" options={options()} />
47
+ }
@@ -0,0 +1,91 @@
1
+ import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
2
+ import { useTheme } from "../../context/theme"
3
+ import { useSync } from "../../context/sync"
4
+ import { useDirectory } from "../../context/directory"
5
+ import { useConnected } from "../../component/use-connected"
6
+ import { createStore } from "solid-js/store"
7
+ import { useRoute } from "../../context/route"
8
+
9
+ export function Footer() {
10
+ const { theme } = useTheme()
11
+ const sync = useSync()
12
+ const route = useRoute()
13
+ const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
14
+ const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
15
+ const lsp = createMemo(() => Object.keys(sync.data.lsp))
16
+ const permissions = createMemo(() => {
17
+ if (route.data.type !== "session") return []
18
+ return sync.data.permission[route.data.sessionID] ?? []
19
+ })
20
+ const directory = useDirectory()
21
+ const connected = useConnected()
22
+
23
+ const [store, setStore] = createStore({
24
+ welcome: false,
25
+ })
26
+
27
+ onMount(() => {
28
+ // Track all timeouts to ensure proper cleanup
29
+ const timeouts: ReturnType<typeof setTimeout>[] = []
30
+
31
+ function tick() {
32
+ if (connected()) return
33
+ if (!store.welcome) {
34
+ setStore("welcome", true)
35
+ timeouts.push(setTimeout(() => tick(), 5000))
36
+ return
37
+ }
38
+
39
+ if (store.welcome) {
40
+ setStore("welcome", false)
41
+ timeouts.push(setTimeout(() => tick(), 10_000))
42
+ return
43
+ }
44
+ }
45
+ timeouts.push(setTimeout(() => tick(), 10_000))
46
+
47
+ onCleanup(() => {
48
+ timeouts.forEach(clearTimeout)
49
+ })
50
+ })
51
+
52
+ return (
53
+ <box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
54
+ <text fg={theme.textMuted}>{directory()}</text>
55
+ <box gap={2} flexDirection="row" flexShrink={0}>
56
+ <Switch>
57
+ <Match when={store.welcome}>
58
+ <text fg={theme.text}>
59
+ Get started <span style={{ fg: theme.textMuted }}>/connect</span>
60
+ </text>
61
+ </Match>
62
+ <Match when={connected()}>
63
+ <Show when={permissions().length > 0}>
64
+ <text fg={theme.warning}>
65
+ <span style={{ fg: theme.warning }}>△</span> {permissions().length} Permission
66
+ {permissions().length > 1 ? "s" : ""}
67
+ </text>
68
+ </Show>
69
+ <text fg={theme.text}>
70
+ <span style={{ fg: lsp().length > 0 ? theme.success : theme.textMuted }}>•</span> {lsp().length} LSP
71
+ </text>
72
+ <Show when={mcp()}>
73
+ <text fg={theme.text}>
74
+ <Switch>
75
+ <Match when={mcpError()}>
76
+ <span style={{ fg: theme.error }}>⊙ </span>
77
+ </Match>
78
+ <Match when={true}>
79
+ <span style={{ fg: theme.success }}>⊙ </span>
80
+ </Match>
81
+ </Switch>
82
+ {mcp()} MCP
83
+ </text>
84
+ </Show>
85
+ <text fg={theme.textMuted}>/status</text>
86
+ </Match>
87
+ </Switch>
88
+ </box>
89
+ </box>
90
+ )
91
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * APX session chat view.
3
+ *
4
+ * A self-contained chat interface that streams messages through the APX daemon
5
+ * using the APX sync context. The complex opencode session view is replaced with
6
+ * a simple but functional chat layout.
7
+ */
8
+ import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
9
+ import { TextareaRenderable } from "@opentui/core"
10
+ import { useTerminalDimensions } from "@opentui/solid"
11
+ import { useTheme } from "@tui/context/theme"
12
+ import { useRoute } from "@tui/context/route"
13
+ import { useApxSync } from "@tui/context/sync-apx"
14
+ import { useToast, Toast } from "@tui/ui/toast"
15
+ import { useExit } from "@tui/context/exit"
16
+ import { usePromptRef } from "@tui/context/prompt"
17
+ import type { ApxMessage } from "@tui/context/sync-apx"
18
+
19
+ function UserBubble(props: { msg: ApxMessage }) {
20
+ const { theme } = useTheme()
21
+ return (
22
+ <box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
23
+ <text color={theme.primary} bold>
24
+ You
25
+ </text>
26
+ <text color={theme.text} wrap>
27
+ {props.msg.text}
28
+ </text>
29
+ </box>
30
+ )
31
+ }
32
+
33
+ function AssistantBubble(props: { msg: ApxMessage }) {
34
+ const { theme } = useTheme()
35
+ const hasText = () => props.msg.text.length > 0
36
+ return (
37
+ <box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
38
+ <text color={theme.success} bold>
39
+ {props.msg.streaming ? "Assistant ▸" : "Assistant"}
40
+ </text>
41
+ <Show when={hasText()} fallback={<text color={theme.textMuted}>…</text>}>
42
+ <text color={props.msg.error ? theme.error : theme.text} wrap>
43
+ {props.msg.text}
44
+ </text>
45
+ </Show>
46
+ </box>
47
+ )
48
+ }
49
+
50
+ export function Session() {
51
+ const dims = useTerminalDimensions()
52
+ const { theme } = useTheme()
53
+ const route = useRoute()
54
+ const sync = useApxSync()
55
+ const toast = useToast()
56
+ const exit = useExit()
57
+ const promptRef = usePromptRef()
58
+ const [sending, setSending] = createSignal(false)
59
+ let inputEl: TextareaRenderable | undefined
60
+
61
+ const sessionID = createMemo(() => {
62
+ if (route.data.type === "session") return route.data.sessionID
63
+ return sync.session.current() ?? ""
64
+ })
65
+
66
+ const messages = createMemo(() => sync.session.messages(sessionID()))
67
+
68
+ onCleanup(() => {
69
+ promptRef.set(undefined)
70
+ })
71
+
72
+ function makeRef(r: TextareaRenderable) {
73
+ return {
74
+ get focused() {
75
+ return r.focused
76
+ },
77
+ get current() {
78
+ return { input: r.plainText, parts: [] as any[] }
79
+ },
80
+ set(prompt: { input: string; parts: any[] }) {
81
+ r.setText(prompt.input)
82
+ },
83
+ reset() {
84
+ r.clear()
85
+ },
86
+ blur() {
87
+ r.blur()
88
+ },
89
+ focus() {
90
+ r.focus()
91
+ },
92
+ submit() {
93
+ void handleSubmit()
94
+ },
95
+ }
96
+ }
97
+
98
+ async function handleSubmit() {
99
+ if (!inputEl) return
100
+ const text = inputEl.plainText.trim()
101
+ if (!text || sending()) return
102
+
103
+ // Check for exit commands
104
+ if (text === "exit" || text === "quit" || text === ":q") {
105
+ void exit()
106
+ return
107
+ }
108
+
109
+ inputEl.clear()
110
+ setSending(true)
111
+ try {
112
+ await sync.sendMessage(text)
113
+ } catch (e) {
114
+ toast.error(e instanceof Error ? e : new Error(String(e)))
115
+ } finally {
116
+ setSending(false)
117
+ }
118
+ }
119
+
120
+ return (
121
+ <box flexDirection="column" flexGrow={1} width={dims().width} height={dims().height}>
122
+ {/* Message list */}
123
+ <scrollbox
124
+ flexGrow={1}
125
+ stickyScroll
126
+ stickyStart="bottom"
127
+ verticalScrollbarOptions={{ visible: true }}
128
+ >
129
+ <box flexDirection="column" width={dims().width}>
130
+ <Show
131
+ when={messages().length > 0}
132
+ fallback={
133
+ <box paddingLeft={2} paddingTop={2}>
134
+ <text color={theme.textMuted} italic>
135
+ Type a message and press Enter to start chatting.
136
+ </text>
137
+ </box>
138
+ }
139
+ >
140
+ <For each={messages()}>
141
+ {(msg) => (
142
+ <Show when={msg.role === "user"} fallback={<AssistantBubble msg={msg} />}>
143
+ <UserBubble msg={msg} />
144
+ </Show>
145
+ )}
146
+ </For>
147
+ </Show>
148
+ <box height={1} />
149
+ </box>
150
+ </scrollbox>
151
+
152
+ {/* Input area */}
153
+ <box
154
+ flexShrink={0}
155
+ flexDirection="column"
156
+ borderTop={1}
157
+ borderColor={theme.border}
158
+ backgroundColor={theme.backgroundElement}
159
+ >
160
+ <box paddingLeft={2} paddingRight={2} paddingTop={1}>
161
+ <textarea
162
+ ref={(r: TextareaRenderable) => {
163
+ inputEl = r
164
+ promptRef.set(makeRef(r))
165
+ }}
166
+ placeholder={sending() ? "Waiting for response…" : "Ask anything... (Enter to send)"}
167
+ placeholderColor={theme.textMuted}
168
+ textColor={theme.text}
169
+ focusedTextColor={theme.text}
170
+ minHeight={1}
171
+ maxHeight={6}
172
+ onSubmit={() => {
173
+ setTimeout(() => setTimeout(() => handleSubmit(), 0), 0)
174
+ }}
175
+ />
176
+ </box>
177
+ <box height={1} paddingLeft={2} paddingRight={2}>
178
+ <Show when={sending()}>
179
+ <text color={theme.textMuted} italic>
180
+ Streaming…
181
+ </text>
182
+ </Show>
183
+ </box>
184
+ </box>
185
+ <Toast />
186
+ </box>
187
+ )
188
+ }