@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,99 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog } from "../ui/dialog"
4
+ import { createStore } from "solid-js/store"
5
+ import { For } from "solid-js"
6
+ import { useBindings } from "../keymap"
7
+
8
+ export function DialogSessionDeleteFailed(props: {
9
+ session: string
10
+ workspace: string
11
+ onDelete?: () => boolean | void | Promise<boolean | void>
12
+ onRestore?: () => boolean | void | Promise<boolean | void>
13
+ onDone?: () => void
14
+ }) {
15
+ const dialog = useDialog()
16
+ const { theme } = useTheme()
17
+ const [store, setStore] = createStore({
18
+ active: "delete" as "delete" | "restore",
19
+ })
20
+
21
+ const options = [
22
+ {
23
+ id: "delete" as const,
24
+ title: "Delete workspace",
25
+ description: "Delete the workspace and all sessions attached to it.",
26
+ run: props.onDelete,
27
+ },
28
+ {
29
+ id: "restore" as const,
30
+ title: "Restore to new workspace",
31
+ description: "Try to restore this session into a new workspace.",
32
+ run: props.onRestore,
33
+ },
34
+ ]
35
+
36
+ async function confirm() {
37
+ const result = await options.find((item) => item.id === store.active)?.run?.()
38
+ if (result === false) return
39
+ props.onDone?.()
40
+ if (!props.onDone) dialog.clear()
41
+ }
42
+
43
+ useBindings(() => ({
44
+ bindings: [
45
+ { key: "return", desc: "Confirm recovery option", group: "Dialog", cmd: () => void confirm() },
46
+ { key: "left", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
47
+ { key: "up", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
48
+ { key: "right", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
49
+ { key: "down", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
50
+ ],
51
+ }))
52
+
53
+ return (
54
+ <box paddingLeft={2} paddingRight={2} gap={1}>
55
+ <box flexDirection="row" justifyContent="space-between">
56
+ <text attributes={TextAttributes.BOLD} fg={theme.text}>
57
+ Failed to Delete Session
58
+ </text>
59
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
60
+ esc
61
+ </text>
62
+ </box>
63
+ <text fg={theme.textMuted} wrapMode="word">
64
+ {`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
65
+ </text>
66
+ <text fg={theme.textMuted} wrapMode="word">
67
+ Choose how you want to recover this broken workspace session.
68
+ </text>
69
+ <box flexDirection="column" paddingBottom={1} gap={1}>
70
+ <For each={options}>
71
+ {(item) => (
72
+ <box
73
+ flexDirection="column"
74
+ paddingLeft={1}
75
+ paddingRight={1}
76
+ paddingTop={1}
77
+ paddingBottom={1}
78
+ backgroundColor={item.id === store.active ? theme.primary : undefined}
79
+ onMouseUp={() => {
80
+ setStore("active", item.id)
81
+ void confirm()
82
+ }}
83
+ >
84
+ <text
85
+ attributes={TextAttributes.BOLD}
86
+ fg={item.id === store.active ? theme.selectedListItemText : theme.text}
87
+ >
88
+ {item.title}
89
+ </text>
90
+ <text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
91
+ {item.description}
92
+ </text>
93
+ </box>
94
+ )}
95
+ </For>
96
+ </box>
97
+ </box>
98
+ )
99
+ }
@@ -0,0 +1,323 @@
1
+ import { useDialog } from "@tui/ui/dialog"
2
+ import { DialogSelect } from "@tui/ui/dialog-select"
3
+ import { useRoute } from "@tui/context/route"
4
+ import { useSync } from "@tui/context/sync"
5
+ import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js"
6
+ import { Locale } from "@/util/locale"
7
+ import { useProject } from "@tui/context/project"
8
+ import { useTheme } from "../context/theme"
9
+ import { useSDK } from "../context/sdk"
10
+ import { useLocal } from "../context/local"
11
+ import { Flag } from "@opencode-ai/core/flag/flag"
12
+ import { DialogSessionRename } from "./dialog-session-rename"
13
+ import { createDebouncedSignal } from "../util/signal"
14
+ import { useToast } from "../ui/toast"
15
+ import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
16
+ import { Spinner } from "./spinner"
17
+ import { errorMessage } from "@/util/error"
18
+ import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
19
+ import { WorkspaceLabel } from "./workspace-label"
20
+ import { useCommandShortcut } from "../keymap"
21
+
22
+ export function DialogSessionList() {
23
+ const dialog = useDialog()
24
+ const route = useRoute()
25
+ const sync = useSync()
26
+ const project = useProject()
27
+ const { theme } = useTheme()
28
+ const sdk = useSDK()
29
+ const local = useLocal()
30
+ const toast = useToast()
31
+ const [toDelete, setToDelete] = createSignal<string>()
32
+ const [search, setSearch] = createDebouncedSignal("", 150)
33
+ const deleteHint = useCommandShortcut("session.delete")
34
+
35
+ const [searchResults, { refetch }] = createResource(
36
+ () => ({ query: search(), filter: sync.session.query() }),
37
+ async (input) => {
38
+ if (!input.query) return undefined
39
+ const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })
40
+ return result.data ?? []
41
+ },
42
+ )
43
+
44
+ const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
45
+ const sessions = createMemo(() => searchResults() ?? sync.data.session)
46
+
47
+ function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
48
+ const workspace = project.workspace.get(session.workspaceID!)
49
+ const list = () => dialog.replace(() => <DialogSessionList />)
50
+ const warp = async (selection: WorkspaceSelection) => {
51
+ const workspaceID = await (async () => {
52
+ if (selection.type === "none") return null
53
+ if (selection.type === "existing") return selection.workspaceID
54
+ const result = await sdk.client.experimental.workspace
55
+ .create({ type: selection.workspaceType, branch: null })
56
+ .catch(() => undefined)
57
+ const workspace = result?.data
58
+ if (!workspace) {
59
+ toast.show({
60
+ message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
61
+ variant: "error",
62
+ })
63
+ return
64
+ }
65
+ await project.workspace.sync()
66
+ return workspace.id
67
+ })()
68
+ if (workspaceID === undefined) return
69
+ await warpWorkspaceSession({
70
+ dialog,
71
+ sdk,
72
+ sync,
73
+ project,
74
+ toast,
75
+ sourceWorkspaceID: session.workspaceID,
76
+ workspaceID,
77
+ sessionID: session.id,
78
+ copyChanges: false,
79
+ done: list,
80
+ })
81
+ }
82
+ dialog.replace(() => (
83
+ <DialogSessionDeleteFailed
84
+ session={session.title}
85
+ workspace={workspace?.name ?? session.workspaceID!}
86
+ onDone={list}
87
+ onDelete={async () => {
88
+ const current = currentSessionID()
89
+ const info = current ? sync.data.session.find((item) => item.id === current) : undefined
90
+ const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
91
+ if (result.error) {
92
+ toast.show({
93
+ variant: "error",
94
+ title: "Failed to delete workspace",
95
+ message: errorMessage(result.error),
96
+ })
97
+ return false
98
+ }
99
+ await project.workspace.sync()
100
+ await sync.session.refresh()
101
+ if (search()) await refetch()
102
+ if (info?.workspaceID === session.workspaceID) {
103
+ route.navigate({ type: "home" })
104
+ }
105
+ return true
106
+ }}
107
+ onRestore={() => {
108
+ void openWorkspaceSelect({
109
+ dialog,
110
+ sdk,
111
+ sync,
112
+ project,
113
+ toast,
114
+ onSelect: (selection) => {
115
+ void warp(selection)
116
+ },
117
+ })
118
+ return false
119
+ }}
120
+ />
121
+ ))
122
+ }
123
+
124
+ function orderByRecency(sessionsList: NonNullable<ReturnType<typeof sessions>>) {
125
+ return sessionsList
126
+ .filter((x) => x.parentID === undefined)
127
+ .toSorted((a, b) => b.time.updated - a.time.updated)
128
+ .map((x) => x.id)
129
+ }
130
+
131
+ const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))
132
+
133
+ const RECENT_LIMIT = 5
134
+
135
+ const options = createMemo(() => {
136
+ const enabled = Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
137
+ const today = new Date().toDateString()
138
+ const sessionMap = new Map(
139
+ sessions()
140
+ .filter((x) => x.parentID === undefined)
141
+ .map((x) => [x.id, x]),
142
+ )
143
+
144
+ const searchResult = searchResults()
145
+ const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
146
+
147
+ const dismissed = enabled ? new Set(local.session.dismissedRecent()) : new Set<string>()
148
+ const pinned = enabled ? local.session.pinned().filter((id) => sessionMap.has(id)) : []
149
+ const pinnedSet = new Set(pinned)
150
+ const slotByID = enabled
151
+ ? new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))
152
+ : new Map<string, number>()
153
+
154
+ const recent = enabled
155
+ ? displayOrder.filter((id) => !pinnedSet.has(id) && !dismissed.has(id)).slice(0, RECENT_LIMIT)
156
+ : []
157
+ const recentSet = new Set(recent)
158
+
159
+ function buildOption(id: string, category: string) {
160
+ const x = sessionMap.get(id)
161
+ if (!x) return undefined
162
+ const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
163
+
164
+ let footer: JSX.Element | string = ""
165
+ if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
166
+ if (x.workspaceID) {
167
+ footer = workspace ? (
168
+ <WorkspaceLabel
169
+ type={workspace.type}
170
+ name={workspace.name}
171
+ status={project.workspace.status(x.workspaceID) ?? "error"}
172
+ />
173
+ ) : (
174
+ <WorkspaceLabel type="unknown" name={x.workspaceID} status="error" />
175
+ )
176
+ }
177
+ } else {
178
+ footer = Locale.time(x.time.updated)
179
+ }
180
+
181
+ const isDeleting = toDelete() === x.id
182
+ const status = sync.data.session_status?.[x.id]
183
+ const isWorking = status?.type === "busy" || status?.type === "retry"
184
+ const slot = slotByID.get(x.id)
185
+ const gutter = isWorking
186
+ ? () => <Spinner />
187
+ : slot !== undefined
188
+ ? () => <text fg={theme.accent}>{slot}</text>
189
+ : undefined
190
+ return {
191
+ title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,
192
+ bg: isDeleting ? theme.error : undefined,
193
+ value: x.id,
194
+ category,
195
+ footer,
196
+ gutter,
197
+ }
198
+ }
199
+
200
+ const remaining = displayOrder
201
+ .filter((id) => !pinnedSet.has(id) && !recentSet.has(id))
202
+ .map((id) => {
203
+ const x = sessionMap.get(id)
204
+ if (!x) return undefined
205
+ const label = new Date(x.time.updated).toDateString()
206
+ return buildOption(id, label === today ? "Today" : label)
207
+ })
208
+ .filter((x) => x !== undefined)
209
+
210
+ return [
211
+ ...pinned.map((id) => buildOption(id, "Pinned")).filter((x) => x !== undefined),
212
+ ...recent.map((id) => buildOption(id, "Recent")).filter((x) => x !== undefined),
213
+ ...remaining,
214
+ ]
215
+ })
216
+
217
+ onMount(() => {
218
+ dialog.setSize("large")
219
+ })
220
+
221
+ return (
222
+ <DialogSelect
223
+ title="Sessions"
224
+ options={options()}
225
+ skipFilter={true}
226
+ current={currentSessionID()}
227
+ onFilter={setSearch}
228
+ onMove={() => {
229
+ setToDelete(undefined)
230
+ }}
231
+ onSelect={(option) => {
232
+ route.navigate({
233
+ type: "session",
234
+ sessionID: option.value,
235
+ })
236
+ dialog.clear()
237
+ }}
238
+ actions={[
239
+ ...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
240
+ ? [
241
+ {
242
+ command: "session.pin.toggle",
243
+ title: "pin/unpin",
244
+ onTrigger: (option: { value: string }) => {
245
+ local.session.togglePin(option.value)
246
+ },
247
+ },
248
+ {
249
+ command: "session.toggle.recent",
250
+ title: "toggle recent",
251
+ onTrigger: (option: { value: string }) => {
252
+ if (local.session.isPinned(option.value)) {
253
+ toast.show({
254
+ variant: "info",
255
+ message: "Unpin the session first to toggle it in Recent",
256
+ duration: 3000,
257
+ })
258
+ return
259
+ }
260
+ local.session.toggleRecent(option.value)
261
+ },
262
+ },
263
+ ]
264
+ : []),
265
+ {
266
+ command: "session.delete",
267
+ title: "delete",
268
+ onTrigger: async (option) => {
269
+ if (toDelete() === option.value) {
270
+ const session = sessions().find((item) => item.id === option.value)
271
+ const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
272
+
273
+ try {
274
+ const result = await sdk.client.session.delete({
275
+ sessionID: option.value,
276
+ })
277
+ if (result.error) {
278
+ if (session?.workspaceID) {
279
+ recover(session)
280
+ } else {
281
+ toast.show({
282
+ variant: "error",
283
+ title: "Failed to delete session",
284
+ message: errorMessage(result.error),
285
+ })
286
+ }
287
+ setToDelete(undefined)
288
+ return
289
+ }
290
+ } catch (err) {
291
+ if (session?.workspaceID) {
292
+ recover(session)
293
+ } else {
294
+ toast.show({
295
+ variant: "error",
296
+ title: "Failed to delete session",
297
+ message: errorMessage(err),
298
+ })
299
+ }
300
+ setToDelete(undefined)
301
+ return
302
+ }
303
+ if (status && status !== "connected") {
304
+ await sync.session.refresh()
305
+ }
306
+ if (search()) await refetch()
307
+ setToDelete(undefined)
308
+ return
309
+ }
310
+ setToDelete(option.value)
311
+ },
312
+ },
313
+ {
314
+ command: "session.rename",
315
+ title: "rename",
316
+ onTrigger: async (option) => {
317
+ dialog.replace(() => <DialogSessionRename session={option.value} />)
318
+ },
319
+ },
320
+ ]}
321
+ />
322
+ )
323
+ }
@@ -0,0 +1,31 @@
1
+ import { DialogPrompt } from "@tui/ui/dialog-prompt"
2
+ import { useDialog } from "@tui/ui/dialog"
3
+ import { useSync } from "@tui/context/sync"
4
+ import { createMemo } from "solid-js"
5
+ import { useSDK } from "../context/sdk"
6
+
7
+ interface DialogSessionRenameProps {
8
+ session: string
9
+ }
10
+
11
+ export function DialogSessionRename(props: DialogSessionRenameProps) {
12
+ const dialog = useDialog()
13
+ const sync = useSync()
14
+ const sdk = useSDK()
15
+ const session = createMemo(() => sync.session.get(props.session))
16
+
17
+ return (
18
+ <DialogPrompt
19
+ title="Rename Session"
20
+ value={session()?.title}
21
+ onConfirm={(value) => {
22
+ void sdk.client.session.update({
23
+ sessionID: props.session,
24
+ title: value,
25
+ })
26
+ dialog.clear()
27
+ }}
28
+ onCancel={() => dialog.clear()}
29
+ />
30
+ )
31
+ }
@@ -0,0 +1,36 @@
1
+ import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
2
+ import { createResource, createMemo } from "solid-js"
3
+ import { useDialog } from "@tui/ui/dialog"
4
+ import { useSDK } from "@tui/context/sdk"
5
+
6
+ export type DialogSkillProps = {
7
+ onSelect: (skill: string) => void
8
+ }
9
+
10
+ export function DialogSkill(props: DialogSkillProps) {
11
+ const dialog = useDialog()
12
+ const sdk = useSDK()
13
+ dialog.setSize("large")
14
+
15
+ const [skills] = createResource(async () => {
16
+ const result = await sdk.client.app.skills()
17
+ return result.data ?? []
18
+ })
19
+
20
+ const options = createMemo<DialogSelectOption<string>[]>(() => {
21
+ const list = skills() ?? []
22
+ const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
23
+ return list.map((skill) => ({
24
+ title: skill.name.padEnd(maxWidth),
25
+ description: skill.description?.replace(/\s+/g, " ").trim(),
26
+ value: skill.name,
27
+ category: "Skills",
28
+ onSelect: () => {
29
+ props.onSelect(skill.name)
30
+ dialog.clear()
31
+ },
32
+ }))
33
+ })
34
+
35
+ return <DialogSelect title="Skills" placeholder="Search skills..." options={options()} />
36
+ }
@@ -0,0 +1,87 @@
1
+ import { useDialog } from "@tui/ui/dialog"
2
+ import { DialogSelect } from "@tui/ui/dialog-select"
3
+ import { createMemo, createSignal } from "solid-js"
4
+ import { Locale } from "@/util/locale"
5
+ import { useTheme } from "../context/theme"
6
+ import { usePromptStash, type StashEntry } from "./prompt/stash"
7
+ import { useCommandShortcut } from "../keymap"
8
+
9
+ function getRelativeTime(timestamp: number): string {
10
+ const now = Date.now()
11
+ const diff = now - timestamp
12
+ const seconds = Math.floor(diff / 1000)
13
+ const minutes = Math.floor(seconds / 60)
14
+ const hours = Math.floor(minutes / 60)
15
+ const days = Math.floor(hours / 24)
16
+
17
+ if (seconds < 60) return "just now"
18
+ if (minutes < 60) return `${minutes}m ago`
19
+ if (hours < 24) return `${hours}h ago`
20
+ if (days < 7) return `${days}d ago`
21
+ return Locale.datetime(timestamp)
22
+ }
23
+
24
+ function getStashPreview(input: string, maxLength: number = 50): string {
25
+ const firstLine = input.split("\n")[0].trim()
26
+ return Locale.truncate(firstLine, maxLength)
27
+ }
28
+
29
+ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
30
+ const dialog = useDialog()
31
+ const stash = usePromptStash()
32
+ const { theme } = useTheme()
33
+
34
+ const [toDelete, setToDelete] = createSignal<number>()
35
+ const deleteHint = useCommandShortcut("stash.delete")
36
+
37
+ const options = createMemo(() => {
38
+ const entries = stash.list()
39
+ // Show most recent first
40
+ return entries
41
+ .map((entry, index) => {
42
+ const isDeleting = toDelete() === index
43
+ const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1
44
+ return {
45
+ title: isDeleting ? `Press ${deleteHint()} again to confirm` : getStashPreview(entry.input),
46
+ bg: isDeleting ? theme.error : undefined,
47
+ value: index,
48
+ description: getRelativeTime(entry.timestamp),
49
+ footer: lineCount > 1 ? `~${lineCount} lines` : undefined,
50
+ }
51
+ })
52
+ .toReversed()
53
+ })
54
+
55
+ return (
56
+ <DialogSelect
57
+ title="Stash"
58
+ options={options()}
59
+ onMove={() => {
60
+ setToDelete(undefined)
61
+ }}
62
+ onSelect={(option) => {
63
+ const entries = stash.list()
64
+ const entry = entries[option.value]
65
+ if (entry) {
66
+ stash.remove(option.value)
67
+ props.onSelect(entry)
68
+ }
69
+ dialog.clear()
70
+ }}
71
+ actions={[
72
+ {
73
+ command: "stash.delete",
74
+ title: "delete",
75
+ onTrigger: (option) => {
76
+ if (toDelete() === option.value) {
77
+ stash.remove(option.value)
78
+ setToDelete(undefined)
79
+ return
80
+ }
81
+ setToDelete(option.value)
82
+ },
83
+ },
84
+ ]}
85
+ />
86
+ )
87
+ }