@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,168 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { fileURLToPath } from "bun"
3
+ import { useTheme } from "../context/theme"
4
+ import { useDialog } from "@tui/ui/dialog"
5
+ import { useSync } from "@tui/context/sync"
6
+ import { For, Match, Switch, Show, createMemo } from "solid-js"
7
+
8
+ export type DialogStatusProps = {}
9
+
10
+ export function DialogStatus() {
11
+ const sync = useSync()
12
+ const { theme } = useTheme()
13
+ const dialog = useDialog()
14
+
15
+ const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
16
+
17
+ const plugins = createMemo(() => {
18
+ const list = sync.data.config.plugin ?? []
19
+ const result = list.map((item) => {
20
+ const value = typeof item === "string" ? item : item[0]
21
+ if (value.startsWith("file://")) {
22
+ const path = fileURLToPath(value)
23
+ const parts = path.split("/")
24
+ const filename = parts.pop() || path
25
+ if (!filename.includes(".")) return { name: filename }
26
+ const basename = filename.split(".")[0]
27
+ if (basename === "index") {
28
+ const dirname = parts.pop()
29
+ const name = dirname || basename
30
+ return { name }
31
+ }
32
+ return { name: basename }
33
+ }
34
+ const index = value.lastIndexOf("@")
35
+ if (index <= 0) return { name: value, version: "latest" }
36
+ const name = value.substring(0, index)
37
+ const version = value.substring(index + 1)
38
+ return { name, version }
39
+ })
40
+ return result.toSorted((a, b) => a.name.localeCompare(b.name))
41
+ })
42
+
43
+ return (
44
+ <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
45
+ <box flexDirection="row" justifyContent="space-between">
46
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
47
+ Status
48
+ </text>
49
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
50
+ esc
51
+ </text>
52
+ </box>
53
+ <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
54
+ <box>
55
+ <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
56
+ <For each={Object.entries(sync.data.mcp)}>
57
+ {([key, item]) => (
58
+ <box flexDirection="row" gap={1}>
59
+ <text
60
+ flexShrink={0}
61
+ style={{
62
+ fg: (
63
+ {
64
+ connected: theme.success,
65
+ failed: theme.error,
66
+ disabled: theme.textMuted,
67
+ needs_auth: theme.warning,
68
+ needs_client_registration: theme.error,
69
+ } as Record<string, typeof theme.success>
70
+ )[item.status],
71
+ }}
72
+ >
73
+
74
+ </text>
75
+ <text fg={theme.text} wrapMode="word">
76
+ <b>{key}</b>{" "}
77
+ <span style={{ fg: theme.textMuted }}>
78
+ <Switch fallback={item.status}>
79
+ <Match when={item.status === "connected"}>Connected</Match>
80
+ <Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
81
+ <Match when={item.status === "disabled"}>Disabled in configuration</Match>
82
+ <Match when={(item.status as string) === "needs_auth"}>
83
+ Needs authentication (run: opencode mcp auth {key})
84
+ </Match>
85
+ <Match when={(item.status as string) === "needs_client_registration" && item}>
86
+ {(val) => (val() as { error: string }).error}
87
+ </Match>
88
+ </Switch>
89
+ </span>
90
+ </text>
91
+ </box>
92
+ )}
93
+ </For>
94
+ </box>
95
+ </Show>
96
+ {sync.data.lsp.length > 0 && (
97
+ <box>
98
+ <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
99
+ <For each={sync.data.lsp}>
100
+ {(item) => (
101
+ <box flexDirection="row" gap={1}>
102
+ <text
103
+ flexShrink={0}
104
+ style={{
105
+ fg: {
106
+ connected: theme.success,
107
+ error: theme.error,
108
+ }[item.status],
109
+ }}
110
+ >
111
+
112
+ </text>
113
+ <text fg={theme.text} wrapMode="word">
114
+ <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
115
+ </text>
116
+ </box>
117
+ )}
118
+ </For>
119
+ </box>
120
+ )}
121
+ <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
122
+ <box>
123
+ <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
124
+ <For each={enabledFormatters()}>
125
+ {(item) => (
126
+ <box flexDirection="row" gap={1}>
127
+ <text
128
+ flexShrink={0}
129
+ style={{
130
+ fg: theme.success,
131
+ }}
132
+ >
133
+
134
+ </text>
135
+ <text wrapMode="word" fg={theme.text}>
136
+ <b>{item.name}</b>
137
+ </text>
138
+ </box>
139
+ )}
140
+ </For>
141
+ </box>
142
+ </Show>
143
+ <Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
144
+ <box>
145
+ <text fg={theme.text}>{plugins().length} Plugins</text>
146
+ <For each={plugins()}>
147
+ {(item) => (
148
+ <box flexDirection="row" gap={1}>
149
+ <text
150
+ flexShrink={0}
151
+ style={{
152
+ fg: theme.success,
153
+ }}
154
+ >
155
+
156
+ </text>
157
+ <text wrapMode="word" fg={theme.text}>
158
+ <b>{item.name}</b>
159
+ {item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
160
+ </text>
161
+ </box>
162
+ )}
163
+ </For>
164
+ </box>
165
+ </Show>
166
+ </box>
167
+ )
168
+ }
@@ -0,0 +1,44 @@
1
+ import { createMemo, createResource } from "solid-js"
2
+ import { DialogSelect } from "@tui/ui/dialog-select"
3
+ import { useDialog } from "@tui/ui/dialog"
4
+ import { useSDK } from "@tui/context/sdk"
5
+ import { createStore } from "solid-js/store"
6
+
7
+ export function DialogTag(props: { onSelect?: (value: string) => void }) {
8
+ const sdk = useSDK()
9
+ const dialog = useDialog()
10
+
11
+ const [store] = createStore({
12
+ filter: "",
13
+ })
14
+
15
+ const [files] = createResource(
16
+ () => [store.filter],
17
+ async () => {
18
+ const result = await sdk.client.find.files({
19
+ query: store.filter,
20
+ })
21
+ if (result.error) return []
22
+ const sliced = (result.data ?? []).slice(0, 5)
23
+ return sliced
24
+ },
25
+ )
26
+
27
+ const options = createMemo(() =>
28
+ (files() ?? []).map((file) => ({
29
+ value: file,
30
+ title: file,
31
+ })),
32
+ )
33
+
34
+ return (
35
+ <DialogSelect
36
+ title="Autocomplete"
37
+ options={options()}
38
+ onSelect={(option) => {
39
+ props.onSelect?.(option.value)
40
+ dialog.clear()
41
+ }}
42
+ />
43
+ )
44
+ }
@@ -0,0 +1,50 @@
1
+ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog } from "../ui/dialog"
4
+ import { onCleanup } from "solid-js"
5
+
6
+ export function DialogThemeList() {
7
+ const theme = useTheme()
8
+ const options = Object.keys(theme.all())
9
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
10
+ .map((value) => ({
11
+ title: value,
12
+ value: value,
13
+ }))
14
+ const dialog = useDialog()
15
+ let confirmed = false
16
+ let ref: DialogSelectRef<string>
17
+ const initial = theme.selected
18
+
19
+ onCleanup(() => {
20
+ if (!confirmed) theme.set(initial)
21
+ })
22
+
23
+ return (
24
+ <DialogSelect
25
+ title="Themes"
26
+ options={options}
27
+ current={initial}
28
+ onMove={(opt) => {
29
+ theme.set(opt.value)
30
+ }}
31
+ onSelect={(opt) => {
32
+ theme.set(opt.value)
33
+ confirmed = true
34
+ dialog.clear()
35
+ }}
36
+ ref={(r) => {
37
+ ref = r
38
+ }}
39
+ onFilter={(query) => {
40
+ if (query.length === 0) {
41
+ theme.set(initial)
42
+ return
43
+ }
44
+
45
+ const first = ref.filtered[0]
46
+ if (first) theme.set(first.value)
47
+ }}
48
+ />
49
+ )
50
+ }
@@ -0,0 +1,39 @@
1
+ import { createMemo } from "solid-js"
2
+ import { useLocal } from "@tui/context/local"
3
+ import { DialogSelect } from "@tui/ui/dialog-select"
4
+ import { useDialog } from "@tui/ui/dialog"
5
+
6
+ export function DialogVariant() {
7
+ const local = useLocal()
8
+ const dialog = useDialog()
9
+
10
+ const options = createMemo(() => {
11
+ return [
12
+ {
13
+ value: "default",
14
+ title: "Default",
15
+ onSelect: () => {
16
+ dialog.clear()
17
+ local.model.variant.set(undefined)
18
+ },
19
+ },
20
+ ...local.model.variant.list().map((variant) => ({
21
+ value: variant,
22
+ title: variant,
23
+ onSelect: () => {
24
+ dialog.clear()
25
+ local.model.variant.set(variant)
26
+ },
27
+ })),
28
+ ]
29
+ })
30
+
31
+ return (
32
+ <DialogSelect<string>
33
+ options={options()}
34
+ title={"Select variant"}
35
+ current={local.model.variant.selected()}
36
+ flat={true}
37
+ />
38
+ )
39
+ }
@@ -0,0 +1,302 @@
1
+ import type { Workspace } from "@opencode-ai/sdk/v2"
2
+ import { useDialog } from "@tui/ui/dialog"
3
+ import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
4
+ import { useSync } from "@tui/context/sync"
5
+ import { useProject } from "@tui/context/project"
6
+ import { useRoute } from "@tui/context/route"
7
+ import { createMemo, createSignal, onMount } from "solid-js"
8
+ import { errorMessage } from "@/util/error"
9
+ import { useSDK } from "../context/sdk"
10
+ import { useToast } from "../ui/toast"
11
+ import { DialogAlert } from "../ui/dialog-alert"
12
+ import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
13
+
14
+ type Adapter = {
15
+ type: string
16
+ name: string
17
+ description: string
18
+ }
19
+
20
+ export type WorkspaceSelection =
21
+ | {
22
+ type: "none"
23
+ }
24
+ | {
25
+ type: "new"
26
+ workspaceType: string
27
+ workspaceName: string
28
+ }
29
+ | {
30
+ type: "existing"
31
+ workspaceID: string
32
+ workspaceType: string
33
+ workspaceName: string
34
+ }
35
+
36
+ type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" }
37
+ type ExistingWorkspaceSelectValue = { workspace: Workspace }
38
+
39
+ export function recentConnectedWorkspaces<WorkspaceInfo extends { id: string; timeUsed: number | string }>(input: {
40
+ workspaces: readonly WorkspaceInfo[]
41
+ status: (workspaceID: string) => string | undefined
42
+ limit?: number
43
+ omitWorkspaceID?: string
44
+ }) {
45
+ const allWorkspaces = input.workspaces.filter((workspace) => input.status(workspace.id) === "connected")
46
+ const workspaces = allWorkspaces.toSorted((a, b) => Number(b.timeUsed) - Number(a.timeUsed))
47
+ const recent = workspaces.slice(0, input.limit ?? 3)
48
+
49
+ return { recent, hasMore: recent.length < workspaces.length }
50
+ }
51
+
52
+ export function warpReminderText(dir: string) {
53
+ return `<system-reminder>The user has changed the current working directory to "${dir}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.</system-reminder>`
54
+ }
55
+
56
+ async function loadWorkspaceAdapters(input: {
57
+ sdk: ReturnType<typeof useSDK>
58
+ sync: ReturnType<typeof useSync>
59
+ toast: ReturnType<typeof useToast>
60
+ }) {
61
+ const dir = input.sync.path.directory || input.sdk.directory
62
+ const url = new URL("/experimental/workspace/adapter", input.sdk.url)
63
+ if (dir) url.searchParams.set("directory", dir)
64
+ const res = await input.sdk
65
+ .fetch(url)
66
+ .then((x) => x.json() as Promise<Adapter[]>)
67
+ .catch(() => undefined)
68
+ if (res) return res
69
+ input.toast.show({
70
+ message: "Failed to load workspace adapters",
71
+ variant: "error",
72
+ })
73
+ }
74
+
75
+ export async function openWorkspaceSelect(input: {
76
+ dialog: ReturnType<typeof useDialog>
77
+ sdk: ReturnType<typeof useSDK>
78
+ sync: ReturnType<typeof useSync>
79
+ project: ReturnType<typeof useProject>
80
+ toast: ReturnType<typeof useToast>
81
+ onSelect: (selection: WorkspaceSelection) => Promise<void> | void
82
+ }) {
83
+ input.dialog.clear()
84
+ await input.sdk.client.experimental.workspace.syncList().catch(() => undefined)
85
+ await input.project.workspace.sync().catch(() => undefined)
86
+ const adapters = await loadWorkspaceAdapters(input)
87
+ if (!adapters) return
88
+ input.dialog.replace(() => <DialogWorkspaceSelect adapters={adapters} onSelect={input.onSelect} />)
89
+ }
90
+
91
+ export async function warpWorkspaceSession(input: {
92
+ dialog: ReturnType<typeof useDialog>
93
+ sdk: ReturnType<typeof useSDK>
94
+ sync: ReturnType<typeof useSync>
95
+ project: ReturnType<typeof useProject>
96
+ toast: ReturnType<typeof useToast>
97
+ sourceWorkspaceID?: string
98
+ workspaceID: string | null
99
+ sessionID: string
100
+ copyChanges: boolean
101
+ done?: () => void
102
+ }): Promise<boolean> {
103
+ const result = await input.sdk.client.experimental.workspace
104
+ .warp({
105
+ id: input.workspaceID,
106
+ sessionID: input.sessionID,
107
+ copyChanges: input.copyChanges,
108
+ })
109
+ .catch(() => undefined)
110
+ if (!result?.data) {
111
+ if (result?.error?.name === "VcsApplyError") {
112
+ await DialogAlert.show(
113
+ input.dialog,
114
+ "Unable to Warp Session",
115
+ "Unable to apply file changes to this workspace. It has existing changes that conflict or is based off a different branch. Session has not been warped.",
116
+ )
117
+ return false
118
+ }
119
+
120
+ input.toast.show({
121
+ message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`,
122
+ variant: "error",
123
+ })
124
+ return false
125
+ }
126
+
127
+ input.project.workspace.set(input.workspaceID)
128
+
129
+ await input.sync.bootstrap({ fatal: false }).catch(() => undefined)
130
+
131
+ const dir = input.project.instance.directory() || input.sync.path.directory
132
+ if (dir) {
133
+ await input.sdk.client.session
134
+ .promptAsync({
135
+ sessionID: input.sessionID,
136
+ workspace: input.workspaceID ?? undefined,
137
+ noReply: true,
138
+ parts: [
139
+ {
140
+ type: "text",
141
+ text: warpReminderText(dir),
142
+ synthetic: true,
143
+ },
144
+ ],
145
+ })
146
+ .catch(() => undefined)
147
+ }
148
+
149
+ await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()])
150
+
151
+ if (input.done) {
152
+ input.done()
153
+ return true
154
+ }
155
+ input.dialog.clear()
156
+ return true
157
+ }
158
+
159
+ export async function confirmWorkspaceFileChanges(input: {
160
+ dialog: ReturnType<typeof useDialog>
161
+ sdk: ReturnType<typeof useSDK>
162
+ sourceWorkspaceID?: string
163
+ }) {
164
+ const status = await input.sdk.client.vcs.status({ workspace: input.sourceWorkspaceID }).catch(() => undefined)
165
+ const fileChangeChoice = status?.data?.length
166
+ ? await DialogWorkspaceFileChanges.show(input.dialog, status.data)
167
+ : "no"
168
+ if (!fileChangeChoice) return
169
+ return fileChangeChoice === "yes"
170
+ }
171
+
172
+ export function DialogWorkspaceSelect(props: {
173
+ adapters?: Adapter[]
174
+ onSelect: (selection: WorkspaceSelection) => Promise<void> | void
175
+ }) {
176
+ const dialog = useDialog()
177
+ const project = useProject()
178
+ const route = useRoute()
179
+ const sync = useSync()
180
+ const sdk = useSDK()
181
+ const toast = useToast()
182
+ const [adapters, setAdapters] = createSignal<Adapter[] | undefined>(props.adapters)
183
+ const omittedWorkspaceID = createMemo(() => (route.data.type === "session" ? project.workspace.current() : undefined))
184
+
185
+ onMount(() => {
186
+ dialog.setSize("medium")
187
+ void (async () => {
188
+ if (adapters()) return
189
+ const res = await loadWorkspaceAdapters({ sdk, sync, toast })
190
+ if (!res) return
191
+ setAdapters(res)
192
+ })()
193
+ })
194
+
195
+ const options = createMemo<DialogSelectOption<WorkspaceSelectValue>[]>(() => {
196
+ const list = adapters()
197
+ if (!list) return []
198
+ const { recent, hasMore } = recentConnectedWorkspaces({
199
+ workspaces: project.workspace.list(),
200
+ status: project.workspace.status,
201
+ omitWorkspaceID: omittedWorkspaceID(),
202
+ })
203
+ return [
204
+ ...list.map((adapter) => ({
205
+ title: adapter.name,
206
+ value: { type: "new" as const, workspaceType: adapter.type, workspaceName: adapter.name },
207
+ description: adapter.description,
208
+ category: "New workspace",
209
+ })),
210
+ {
211
+ title: "None",
212
+ value: { type: "none" as const },
213
+ description: "Use the local project",
214
+ category: "Choose workspace",
215
+ },
216
+ ...recent.map((workspace: Workspace) => ({
217
+ title: workspace.name,
218
+ description: `(${workspace.type})`,
219
+ value: {
220
+ type: "existing" as const,
221
+ workspaceID: workspace.id,
222
+ workspaceType: workspace.type,
223
+ workspaceName: workspace.name,
224
+ },
225
+ category: "Choose workspace",
226
+ })),
227
+ ...(hasMore
228
+ ? [
229
+ {
230
+ title: "View all workspaces",
231
+ value: { type: "existing-list" as const },
232
+ description: "Choose from all workspaces",
233
+ category: "Choose workspace",
234
+ },
235
+ ]
236
+ : []),
237
+ ]
238
+ })
239
+
240
+ if (!adapters()) return null
241
+ return (
242
+ <DialogSelect<WorkspaceSelectValue>
243
+ title="Warp"
244
+ skipFilter={true}
245
+ renderFilter={false}
246
+ options={options()}
247
+ onSelect={(option) => {
248
+ if (!option.value) return
249
+ if (option.value.type === "none") {
250
+ void props.onSelect(option.value)
251
+ return
252
+ }
253
+ if (option.value.type === "new") {
254
+ void props.onSelect(option.value)
255
+ return
256
+ }
257
+ if (option.value.type === "existing") {
258
+ void props.onSelect(option.value)
259
+ return
260
+ }
261
+
262
+ dialog.replace(() => (
263
+ <DialogExistingWorkspaceSelect omitWorkspaceID={omittedWorkspaceID()} onSelect={props.onSelect} />
264
+ ))
265
+ }}
266
+ />
267
+ )
268
+ }
269
+
270
+ function DialogExistingWorkspaceSelect(props: {
271
+ omitWorkspaceID?: string
272
+ onSelect: (selection: WorkspaceSelection) => Promise<void> | void
273
+ }) {
274
+ const project = useProject()
275
+
276
+ const options = createMemo<DialogSelectOption<ExistingWorkspaceSelectValue>[]>(() =>
277
+ project.workspace
278
+ .list()
279
+ .filter((workspace) => project.workspace.status(workspace.id) === "connected")
280
+ .filter((workspace) => workspace.id !== props.omitWorkspaceID)
281
+ .map((workspace: Workspace) => ({
282
+ title: workspace.name,
283
+ description: `(${workspace.type})`,
284
+ value: { workspace },
285
+ })),
286
+ )
287
+
288
+ return (
289
+ <DialogSelect<ExistingWorkspaceSelectValue>
290
+ title="Existing Workspace"
291
+ options={options()}
292
+ onSelect={(option) => {
293
+ void props.onSelect({
294
+ type: "existing",
295
+ workspaceID: option.value.workspace.id,
296
+ workspaceType: option.value.workspace.type,
297
+ workspaceName: option.value.workspace.name,
298
+ })
299
+ }}
300
+ />
301
+ )
302
+ }