@agentprojectcontext/apx 1.15.5 → 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 (222) 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/wakeup.js +14 -19
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,608 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+ import { batch, createEffect, createMemo, on } from "solid-js"
4
+ import { useSync } from "@tui/context/sync"
5
+ import { useTheme } from "@tui/context/theme"
6
+ import { useRoute } from "@tui/context/route"
7
+ import { useEvent } from "@tui/context/event"
8
+ import { uniqueBy } from "remeda"
9
+ import path from "path"
10
+ import { Global } from "@opencode-ai/core/global"
11
+ import { Flag } from "@opencode-ai/core/flag/flag"
12
+ import { iife } from "@/util/iife"
13
+ import { useToast } from "../ui/toast"
14
+ import { useArgs } from "./args"
15
+ import { useSDK } from "./sdk"
16
+ import { RGBA } from "@opentui/core"
17
+ import { Filesystem } from "@/util/filesystem"
18
+
19
+ export function parseModel(model: string) {
20
+ const [providerID, ...rest] = model.split("/")
21
+ return {
22
+ providerID: providerID,
23
+ modelID: rest.join("/"),
24
+ }
25
+ }
26
+
27
+ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
28
+ name: "Local",
29
+ init: () => {
30
+ const sync = useSync()
31
+ const sdk = useSDK()
32
+ const toast = useToast()
33
+
34
+ function isModelValid(model: { providerID: string; modelID: string }) {
35
+ const provider = sync.data.provider.find((x) => x.id === model.providerID)
36
+ return !!provider?.models[model.modelID]
37
+ }
38
+
39
+ function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
40
+ for (const modelFn of modelFns) {
41
+ const model = modelFn()
42
+ if (!model) continue
43
+ if (isModelValid(model)) return model
44
+ }
45
+ }
46
+
47
+ const agent = iife(() => {
48
+ const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
49
+ const visibleAgents = createMemo(() => sync.data.agent.filter((x) => !x.hidden))
50
+ const [agentStore, setAgentStore] = createStore({
51
+ current: undefined as string | undefined,
52
+ })
53
+ const { theme } = useTheme()
54
+ const colors = createMemo(() => [
55
+ theme.secondary,
56
+ theme.accent,
57
+ theme.success,
58
+ theme.warning,
59
+ theme.primary,
60
+ theme.error,
61
+ theme.info,
62
+ ])
63
+ return {
64
+ list() {
65
+ return agents()
66
+ },
67
+ current() {
68
+ return agents().find((x) => x.name === agentStore.current) ?? agents().at(0)
69
+ },
70
+ set(name: string) {
71
+ if (!agents().some((x) => x.name === name))
72
+ return toast.show({
73
+ variant: "warning",
74
+ message: `Agent not found: ${name}`,
75
+ duration: 3000,
76
+ })
77
+ setAgentStore("current", name)
78
+ },
79
+ move(direction: 1 | -1) {
80
+ batch(() => {
81
+ const current = this.current()
82
+ if (!current) return
83
+ let next = agents().findIndex((x) => x.name === current.name) + direction
84
+ if (next < 0) next = agents().length - 1
85
+ if (next >= agents().length) next = 0
86
+ const value = agents()[next]
87
+ setAgentStore("current", value.name)
88
+ })
89
+ },
90
+ color(name: string) {
91
+ const index = visibleAgents().findIndex((x) => x.name === name)
92
+ if (index === -1) return colors()[0]
93
+ const agent = visibleAgents()[index]
94
+
95
+ if (agent?.color) {
96
+ const color = agent.color
97
+ if (color.startsWith("#")) return RGBA.fromHex(color)
98
+ // already validated by config, just satisfying TS here
99
+ return theme[color as keyof typeof theme] as RGBA
100
+ }
101
+ return colors()[index % colors().length]
102
+ },
103
+ }
104
+ })
105
+
106
+ const model = iife(() => {
107
+ const [modelStore, setModelStore] = createStore<{
108
+ ready: boolean
109
+ model: Record<
110
+ string,
111
+ {
112
+ providerID: string
113
+ modelID: string
114
+ }
115
+ >
116
+ recent: {
117
+ providerID: string
118
+ modelID: string
119
+ }[]
120
+ favorite: {
121
+ providerID: string
122
+ modelID: string
123
+ }[]
124
+ variant: Record<string, string | undefined>
125
+ }>({
126
+ ready: false,
127
+ model: {},
128
+ recent: [],
129
+ favorite: [],
130
+ variant: {},
131
+ })
132
+
133
+ const filePath = path.join(Global.Path.state, "model.json")
134
+ const state = {
135
+ pending: false,
136
+ }
137
+
138
+ function save() {
139
+ if (!modelStore.ready) {
140
+ state.pending = true
141
+ return
142
+ }
143
+ state.pending = false
144
+ void Filesystem.writeJson(filePath, {
145
+ recent: modelStore.recent,
146
+ favorite: modelStore.favorite,
147
+ variant: modelStore.variant,
148
+ })
149
+ }
150
+
151
+ Filesystem.readJson(filePath)
152
+ .then((x: any) => {
153
+ if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
154
+ if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
155
+ if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
156
+ })
157
+ .catch(() => {})
158
+ .finally(() => {
159
+ setModelStore("ready", true)
160
+ if (state.pending) save()
161
+ })
162
+
163
+ const args = useArgs()
164
+ const fallbackModel = createMemo(() => {
165
+ if (args.model) {
166
+ const { providerID, modelID } = parseModel(args.model)
167
+ if (isModelValid({ providerID, modelID })) {
168
+ return {
169
+ providerID,
170
+ modelID,
171
+ }
172
+ }
173
+ }
174
+
175
+ if (sync.data.config.model) {
176
+ const { providerID, modelID } = parseModel(sync.data.config.model)
177
+ if (isModelValid({ providerID, modelID })) {
178
+ return {
179
+ providerID,
180
+ modelID,
181
+ }
182
+ }
183
+ }
184
+
185
+ for (const item of modelStore.recent) {
186
+ if (isModelValid(item)) {
187
+ return item
188
+ }
189
+ }
190
+
191
+ const provider = sync.data.provider[0]
192
+ if (!provider) return undefined
193
+ const defaultModel = sync.data.provider_default[provider.id]
194
+ const firstModel = Object.values(provider.models)[0]
195
+ const model = defaultModel ?? firstModel?.id
196
+ if (!model) return undefined
197
+ return {
198
+ providerID: provider.id,
199
+ modelID: model,
200
+ }
201
+ })
202
+
203
+ const currentModel = createMemo(() => {
204
+ const a = agent.current()
205
+ return (
206
+ getFirstValidModel(
207
+ () => a && modelStore.model[a.name],
208
+ () => a && a.model,
209
+ fallbackModel,
210
+ ) ?? undefined
211
+ )
212
+ })
213
+
214
+ return {
215
+ current: currentModel,
216
+ get ready() {
217
+ return modelStore.ready
218
+ },
219
+ recent() {
220
+ return modelStore.recent
221
+ },
222
+ favorite() {
223
+ return modelStore.favorite
224
+ },
225
+ parsed: createMemo(() => {
226
+ const value = currentModel()
227
+ if (!value) {
228
+ return {
229
+ provider: "Connect a provider",
230
+ model: "No provider selected",
231
+ reasoning: false,
232
+ }
233
+ }
234
+ const provider = sync.data.provider.find((x) => x.id === value.providerID)
235
+ const info = provider?.models[value.modelID]
236
+ return {
237
+ provider: provider?.name ?? value.providerID,
238
+ model: info?.name ?? value.modelID,
239
+ reasoning: info?.capabilities?.reasoning ?? false,
240
+ }
241
+ }),
242
+ cycle(direction: 1 | -1) {
243
+ const current = currentModel()
244
+ if (!current) return
245
+ const recent = modelStore.recent
246
+ const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
247
+ if (index === -1) return
248
+ let next = index + direction
249
+ if (next < 0) next = recent.length - 1
250
+ if (next >= recent.length) next = 0
251
+ const val = recent[next]
252
+ if (!val) return
253
+ const a = agent.current()
254
+ if (!a) return
255
+ setModelStore("model", a.name, { ...val })
256
+ },
257
+ cycleFavorite(direction: 1 | -1) {
258
+ const favorites = modelStore.favorite.filter((item) => isModelValid(item))
259
+ if (!favorites.length) {
260
+ toast.show({
261
+ variant: "info",
262
+ message: "Add a favorite model to use this shortcut",
263
+ duration: 3000,
264
+ })
265
+ return
266
+ }
267
+ const current = currentModel()
268
+ let index = -1
269
+ if (current) {
270
+ index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
271
+ }
272
+ if (index === -1) {
273
+ index = direction === 1 ? 0 : favorites.length - 1
274
+ } else {
275
+ index += direction
276
+ if (index < 0) index = favorites.length - 1
277
+ if (index >= favorites.length) index = 0
278
+ }
279
+ const next = favorites[index]
280
+ if (!next) return
281
+ const a = agent.current()
282
+ if (!a) return
283
+ setModelStore("model", a.name, { ...next })
284
+ const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
285
+ if (uniq.length > 10) uniq.pop()
286
+ setModelStore(
287
+ "recent",
288
+ uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
289
+ )
290
+ save()
291
+ },
292
+ set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
293
+ batch(() => {
294
+ if (!isModelValid(model)) {
295
+ toast.show({
296
+ message: `Model ${model.providerID}/${model.modelID} is not valid`,
297
+ variant: "warning",
298
+ duration: 3000,
299
+ })
300
+ return
301
+ }
302
+ const a = agent.current()
303
+ if (!a) return
304
+ setModelStore("model", a.name, model)
305
+ if (options?.recent) {
306
+ const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
307
+ if (uniq.length > 10) uniq.pop()
308
+ setModelStore(
309
+ "recent",
310
+ uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
311
+ )
312
+ save()
313
+ }
314
+ })
315
+ },
316
+ toggleFavorite(model: { providerID: string; modelID: string }) {
317
+ batch(() => {
318
+ if (!isModelValid(model)) {
319
+ toast.show({
320
+ message: `Model ${model.providerID}/${model.modelID} is not valid`,
321
+ variant: "warning",
322
+ duration: 3000,
323
+ })
324
+ return
325
+ }
326
+ const exists = modelStore.favorite.some(
327
+ (x) => x.providerID === model.providerID && x.modelID === model.modelID,
328
+ )
329
+ const next = exists
330
+ ? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
331
+ : [model, ...modelStore.favorite]
332
+ setModelStore(
333
+ "favorite",
334
+ next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
335
+ )
336
+ save()
337
+ })
338
+ },
339
+ variant: {
340
+ selected() {
341
+ const m = currentModel()
342
+ if (!m) return undefined
343
+ const key = `${m.providerID}/${m.modelID}`
344
+ return modelStore.variant[key]
345
+ },
346
+ current() {
347
+ const v = this.selected()
348
+ if (!v) return undefined
349
+ if (!this.list().includes(v)) return undefined
350
+ return v
351
+ },
352
+ list() {
353
+ const m = currentModel()
354
+ if (!m) return []
355
+ const provider = sync.data.provider.find((x) => x.id === m.providerID)
356
+ const info = provider?.models[m.modelID]
357
+ if (!info?.variants) return []
358
+ return Object.keys(info.variants)
359
+ },
360
+ set(value: string | undefined) {
361
+ const m = currentModel()
362
+ if (!m) return
363
+ const key = `${m.providerID}/${m.modelID}`
364
+ setModelStore("variant", key, value ?? "default")
365
+ save()
366
+ },
367
+ cycle() {
368
+ const variants = this.list()
369
+ if (variants.length === 0) return
370
+ const current = this.current()
371
+ if (!current) {
372
+ this.set(variants[0])
373
+ return
374
+ }
375
+ const index = variants.indexOf(current)
376
+ if (index === -1 || index === variants.length - 1) {
377
+ this.set(undefined)
378
+ return
379
+ }
380
+ this.set(variants[index + 1])
381
+ },
382
+ },
383
+ }
384
+ })
385
+
386
+ const session = iife(() => {
387
+ const [sessionStore, setSessionStore] = createStore<{
388
+ ready: boolean
389
+ pinned: string[]
390
+ dismissedRecent: string[]
391
+ recentOrder: string[]
392
+ }>({
393
+ ready: false,
394
+ pinned: [],
395
+ dismissedRecent: [],
396
+ recentOrder: [],
397
+ })
398
+
399
+ const filePath = path.join(Global.Path.state, "session.json")
400
+ const state = {
401
+ pending: false,
402
+ }
403
+
404
+ function save() {
405
+ if (!sessionStore.ready) {
406
+ state.pending = true
407
+ return
408
+ }
409
+ state.pending = false
410
+ void Filesystem.writeJson(filePath, {
411
+ pinned: sessionStore.pinned,
412
+ dismissedRecent: sessionStore.dismissedRecent,
413
+ recentOrder: sessionStore.recentOrder,
414
+ })
415
+ }
416
+
417
+ Filesystem.readJson(filePath)
418
+ .then((x: any) => {
419
+ if (Array.isArray(x.pinned)) setSessionStore("pinned", x.pinned)
420
+ if (Array.isArray(x.dismissedRecent)) setSessionStore("dismissedRecent", x.dismissedRecent)
421
+ if (Array.isArray(x.recentOrder)) setSessionStore("recentOrder", x.recentOrder)
422
+ })
423
+ .catch(() => {})
424
+ .finally(() => {
425
+ setSessionStore("ready", true)
426
+ if (state.pending) save()
427
+ })
428
+
429
+ const route = useRoute()
430
+ const event = useEvent()
431
+ let cycling = false
432
+
433
+ const slots = createMemo(() => {
434
+ const rootSessions = sync.data.session.filter((x) => x.parentID === undefined)
435
+ const existing = new Set(rootSessions.map((x) => x.id))
436
+ const dismissed = new Set(sessionStore.dismissedRecent)
437
+ const pins = sessionStore.pinned.filter((id) => existing.has(id))
438
+ const pinnedSet = new Set(pins)
439
+ const recent = rootSessions
440
+ .filter((x) => !pinnedSet.has(x.id) && !dismissed.has(x.id))
441
+ .toSorted((a, b) => b.time.updated - a.time.updated)
442
+ .map((x) => x.id)
443
+ return [...pins, ...recent].slice(0, 9)
444
+ })
445
+
446
+ function prune(sessionID: string) {
447
+ batch(() => {
448
+ if (sessionStore.pinned.includes(sessionID)) {
449
+ setSessionStore(
450
+ "pinned",
451
+ sessionStore.pinned.filter((x) => x !== sessionID),
452
+ )
453
+ }
454
+ if (sessionStore.dismissedRecent.includes(sessionID)) {
455
+ setSessionStore(
456
+ "dismissedRecent",
457
+ sessionStore.dismissedRecent.filter((x) => x !== sessionID),
458
+ )
459
+ }
460
+ if (sessionStore.recentOrder.includes(sessionID)) {
461
+ setSessionStore(
462
+ "recentOrder",
463
+ sessionStore.recentOrder.filter((x) => x !== sessionID),
464
+ )
465
+ }
466
+ save()
467
+ })
468
+ }
469
+
470
+ event.on("session.deleted", (evt) => {
471
+ prune(evt.properties.info.id)
472
+ })
473
+
474
+ if (Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING) {
475
+ createEffect(
476
+ on(
477
+ () => (sessionStore.ready && route.data.type === "session" ? route.data.sessionID : undefined),
478
+ (sessionID) => {
479
+ if (!sessionID) return
480
+ if (cycling) {
481
+ cycling = false
482
+ return
483
+ }
484
+ const filtered = sessionStore.recentOrder.filter((x) => x !== sessionID)
485
+ const next = [sessionID, ...filtered].slice(0, 20)
486
+ setSessionStore("recentOrder", next)
487
+ save()
488
+ },
489
+ ),
490
+ )
491
+ }
492
+
493
+ return {
494
+ get ready() {
495
+ return sessionStore.ready
496
+ },
497
+ pinned() {
498
+ return sessionStore.pinned
499
+ },
500
+ dismissedRecent() {
501
+ return sessionStore.dismissedRecent
502
+ },
503
+ recentOrder() {
504
+ return sessionStore.recentOrder
505
+ },
506
+ slots,
507
+ isPinned(sessionID: string) {
508
+ return sessionStore.pinned.includes(sessionID)
509
+ },
510
+ isDismissed(sessionID: string) {
511
+ return sessionStore.dismissedRecent.includes(sessionID)
512
+ },
513
+ togglePin(sessionID: string) {
514
+ batch(() => {
515
+ const exists = sessionStore.pinned.includes(sessionID)
516
+ const next = exists
517
+ ? sessionStore.pinned.filter((x) => x !== sessionID)
518
+ : [sessionID, ...sessionStore.pinned]
519
+ setSessionStore("pinned", next)
520
+ save()
521
+ })
522
+ },
523
+ toggleRecent(sessionID: string) {
524
+ batch(() => {
525
+ const exists = sessionStore.dismissedRecent.includes(sessionID)
526
+ const next = exists
527
+ ? sessionStore.dismissedRecent.filter((x) => x !== sessionID)
528
+ : [sessionID, ...sessionStore.dismissedRecent]
529
+ setSessionStore("dismissedRecent", next)
530
+ save()
531
+ })
532
+ },
533
+ quickSwitch(slot: number) {
534
+ const target = slots()[slot - 1]
535
+ if (!target) return
536
+ if (route.data.type === "session" && route.data.sessionID === target) return
537
+ route.navigate({ type: "session", sessionID: target })
538
+ },
539
+ cycleRecent(direction: 1 | -1) {
540
+ if (route.data.type !== "session") {
541
+ toast.show({
542
+ variant: "info",
543
+ message: "Open a session first to cycle between recent sessions",
544
+ duration: 3000,
545
+ })
546
+ return
547
+ }
548
+ const current = route.data.sessionID
549
+ const order = sessionStore.recentOrder.filter((id) =>
550
+ sync.data.session.some((s) => s.id === id && s.parentID === undefined),
551
+ )
552
+ if (order.length < 2) {
553
+ toast.show({
554
+ variant: "info",
555
+ message: "No other recent sessions to cycle to",
556
+ duration: 3000,
557
+ })
558
+ return
559
+ }
560
+ const index = order.indexOf(current)
561
+ if (index === -1) return
562
+ const next = index + direction
563
+ if (next < 0 || next >= order.length) return
564
+ const target = order[next]
565
+ if (!target || target === current) return
566
+ cycling = true
567
+ route.navigate({ type: "session", sessionID: target })
568
+ },
569
+ }
570
+ })
571
+
572
+ const mcp = {
573
+ isEnabled(name: string) {
574
+ const status = sync.data.mcp[name]
575
+ return status?.status === "connected"
576
+ },
577
+ async toggle(name: string) {
578
+ const status = sync.data.mcp[name]
579
+ if (status?.status === "connected") {
580
+ // Disable: disconnect the MCP
581
+ await sdk.client.mcp.disconnect({ name })
582
+ } else {
583
+ // Enable/Retry: connect the MCP (handles disabled, failed, and other states)
584
+ await sdk.client.mcp.connect({ name })
585
+ }
586
+ },
587
+ }
588
+
589
+ createEffect(() => {
590
+ const value = agent.current()
591
+ if (!value?.model) return
592
+ if (isModelValid(value.model)) return
593
+ toast.show({
594
+ variant: "warning",
595
+ message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
596
+ duration: 3000,
597
+ })
598
+ })
599
+
600
+ const result = {
601
+ model,
602
+ agent,
603
+ mcp,
604
+ session,
605
+ }
606
+ return result
607
+ },
608
+ })
@@ -0,0 +1,39 @@
1
+ import path from "path"
2
+ import { createContext, useContext, type ParentProps } from "solid-js"
3
+ import { Global } from "@opencode-ai/core/global"
4
+
5
+ const context = createContext<{
6
+ path: () => string
7
+ format: (input?: string) => string
8
+ }>()
9
+
10
+ export function PathFormatterProvider(props: ParentProps<{ path: string | undefined }>) {
11
+ return (
12
+ <context.Provider
13
+ value={{ path: () => props.path || process.cwd(), format: (input) => formatPath(input, props.path) }}
14
+ >
15
+ {props.children}
16
+ </context.Provider>
17
+ )
18
+ }
19
+
20
+ export function usePathFormatter() {
21
+ const value = useContext(context)
22
+ if (!value) throw new Error("PathFormatter context must be used within a PathFormatterProvider")
23
+ return value
24
+ }
25
+
26
+ function formatPath(input: string | undefined, base: string | undefined) {
27
+ if (!input) return ""
28
+
29
+ const root = base || process.cwd()
30
+ const absolute = path.isAbsolute(input) ? input : path.resolve(root, input)
31
+ const relative = path.relative(root, absolute)
32
+
33
+ if (!relative) return "."
34
+ if (relative !== ".." && !relative.startsWith(".." + path.sep)) return relative
35
+ if (Global.Path.home && (absolute === Global.Path.home || absolute.startsWith(Global.Path.home + path.sep))) {
36
+ return absolute.replace(Global.Path.home, "~")
37
+ }
38
+ return absolute
39
+ }
@@ -0,0 +1,48 @@
1
+ import { createSimpleContext } from "./helper"
2
+ import { useSDK } from "./sdk-apx"
3
+
4
+ export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
5
+ name: "Project",
6
+ init: () => {
7
+ const sdk = useSDK()
8
+
9
+ return {
10
+ data: {
11
+ project: { id: sdk.pid },
12
+ instance: { path: { directory: process.cwd(), home: "", state: "", config: "", worktree: "" } },
13
+ workspace: { current: undefined as string | undefined, list: [] as any[], status: {} as Record<string, any> },
14
+ },
15
+ project() {
16
+ return sdk.pid
17
+ },
18
+ instance: {
19
+ path() {
20
+ return { directory: process.cwd(), worktree: "", home: "", state: "", config: "" }
21
+ },
22
+ directory() {
23
+ return process.cwd()
24
+ },
25
+ },
26
+ workspace: {
27
+ current(): string | undefined {
28
+ return undefined
29
+ },
30
+ set(_next?: string | null) {},
31
+ list() {
32
+ return [] as any[]
33
+ },
34
+ get(_id: string) {
35
+ return undefined
36
+ },
37
+ status(_id: string): "connected" | "connecting" | "disconnected" | "error" | undefined {
38
+ return "connected"
39
+ },
40
+ statuses() {
41
+ return {} as Record<string, any>
42
+ },
43
+ async sync() {},
44
+ },
45
+ async sync() {},
46
+ }
47
+ },
48
+ })
@@ -0,0 +1,7 @@
1
+ /**
2
+ * APX compatibility shim for the opencode Project context.
3
+ *
4
+ * Delegates to project-apx.tsx while re-exporting `useProject` and `ProjectProvider`
5
+ * under the same names so existing imports are not broken.
6
+ */
7
+ export { useProject, ProjectProvider } from "./project-apx"