saeeol 1.2.1 → 1.2.3

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 (151) hide show
  1. package/bin/saeeol.cjs +187 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +12 -12
  4. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  20. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  21. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  22. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  23. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  24. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  25. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  26. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  27. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  28. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  29. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  30. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  31. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  32. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  33. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  34. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  35. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  36. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  37. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  38. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  39. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  40. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  41. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  42. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  43. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  44. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  45. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  46. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  47. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  48. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  49. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  50. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  51. package/src/cli/cmd/tui/context/args.tsx +1 -15
  52. package/src/cli/cmd/tui/context/directory.ts +1 -15
  53. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  54. package/src/cli/cmd/tui/context/editor.ts +1 -425
  55. package/src/cli/cmd/tui/context/event.ts +1 -45
  56. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  57. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  58. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  59. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  60. package/src/cli/cmd/tui/context/local.tsx +1 -478
  61. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  62. package/src/cli/cmd/tui/context/project.tsx +1 -109
  63. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  64. package/src/cli/cmd/tui/context/route.tsx +1 -67
  65. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  66. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  67. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  68. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  69. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  70. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  71. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  72. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  74. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  75. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  76. package/src/tool/apply_patch.ts +1 -334
  77. package/src/tool/bash.ts +1 -656
  78. package/src/tool/core/external-directory.ts +55 -0
  79. package/src/tool/core/invalid.ts +21 -0
  80. package/src/tool/core/recall.ts +164 -0
  81. package/src/tool/core/recall.txt +12 -0
  82. package/src/tool/core/schema.ts +16 -0
  83. package/src/tool/core/tool.ts +162 -0
  84. package/src/tool/core/truncate.ts +160 -0
  85. package/src/tool/core/truncation-dir.ts +4 -0
  86. package/src/tool/diagnostics.ts +1 -20
  87. package/src/tool/edit-replacers.ts +1 -288
  88. package/src/tool/edit-utils.ts +1 -86
  89. package/src/tool/edit.ts +1 -262
  90. package/src/tool/external-directory.ts +1 -55
  91. package/src/tool/file/apply_patch.ts +334 -0
  92. package/src/tool/file/apply_patch.txt +33 -0
  93. package/src/tool/file/bash.ts +656 -0
  94. package/src/tool/file/bash.txt +119 -0
  95. package/src/tool/file/edit-replacers.ts +288 -0
  96. package/src/tool/file/edit-utils.ts +86 -0
  97. package/src/tool/file/edit.ts +262 -0
  98. package/src/tool/file/edit.txt +10 -0
  99. package/src/tool/file/read.ts +389 -0
  100. package/src/tool/file/read.txt +14 -0
  101. package/src/tool/file/write.ts +114 -0
  102. package/src/tool/file/write.txt +8 -0
  103. package/src/tool/glob.ts +1 -115
  104. package/src/tool/grep.ts +1 -151
  105. package/src/tool/integration/diagnostics.ts +20 -0
  106. package/src/tool/integration/lsp.ts +113 -0
  107. package/src/tool/integration/lsp.txt +24 -0
  108. package/src/tool/integration/mcp-exa.ts +73 -0
  109. package/src/tool/integration/package.ts +168 -0
  110. package/src/tool/integration/registry.ts +375 -0
  111. package/src/tool/invalid.ts +1 -21
  112. package/src/tool/lsp.ts +1 -113
  113. package/src/tool/mcp-exa.ts +1 -73
  114. package/src/tool/package.ts +1 -168
  115. package/src/tool/plan.ts +1 -30
  116. package/src/tool/question.ts +1 -52
  117. package/src/tool/read.ts +1 -389
  118. package/src/tool/recall.ts +1 -164
  119. package/src/tool/registry.ts +1 -375
  120. package/src/tool/schema.ts +1 -16
  121. package/src/tool/search/glob.ts +115 -0
  122. package/src/tool/search/glob.txt +6 -0
  123. package/src/tool/search/grep.ts +151 -0
  124. package/src/tool/search/grep.txt +8 -0
  125. package/src/tool/search/warpgrep.ts +107 -0
  126. package/src/tool/search/warpgrep.txt +10 -0
  127. package/src/tool/search/webfetch.ts +202 -0
  128. package/src/tool/search/webfetch.txt +13 -0
  129. package/src/tool/search/websearch.ts +71 -0
  130. package/src/tool/search/websearch.txt +14 -0
  131. package/src/tool/skill.ts +1 -91
  132. package/src/tool/task.ts +1 -197
  133. package/src/tool/todo.ts +1 -62
  134. package/src/tool/tool.ts +1 -162
  135. package/src/tool/truncate.ts +1 -160
  136. package/src/tool/truncation-dir.ts +1 -4
  137. package/src/tool/warpgrep.ts +1 -107
  138. package/src/tool/webfetch.ts +1 -202
  139. package/src/tool/websearch.ts +1 -71
  140. package/src/tool/workflow/plan-enter.txt +14 -0
  141. package/src/tool/workflow/plan-exit.txt +13 -0
  142. package/src/tool/workflow/plan.ts +30 -0
  143. package/src/tool/workflow/question.ts +52 -0
  144. package/src/tool/workflow/question.txt +11 -0
  145. package/src/tool/workflow/skill.ts +91 -0
  146. package/src/tool/workflow/skill.txt +5 -0
  147. package/src/tool/workflow/task.ts +197 -0
  148. package/src/tool/workflow/task.txt +57 -0
  149. package/src/tool/workflow/todo.ts +62 -0
  150. package/src/tool/workflow/todowrite.txt +167 -0
  151. package/src/tool/write.ts +1 -114
@@ -0,0 +1,301 @@
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 } from "solid-js"
6
+ import { Locale } from "@/util/locale"
7
+ import { useProject } from "@tui/context/project"
8
+ import { useKeybind } from "../../context/keybind"
9
+ import { useTheme } from "../../context/theme"
10
+ import { useSDK } from "../../context/sdk"
11
+ import { Flag } from "@saeeol/core/flag/flag"
12
+ import { DialogSessionRename } from "./dialog-session-rename"
13
+ import { Keybind } from "@/util/keybind"
14
+ import { createDebouncedSignal } from "../../util/signal"
15
+ import { useToast } from "../../ui/toast"
16
+ import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
17
+ import { Spinner } from "../spinner"
18
+ import path from "path"
19
+ import { errorMessage } from "@/util/error"
20
+ import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
21
+
22
+ type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
23
+
24
+ export function DialogSessionList() {
25
+ const dialog = useDialog()
26
+ const route = useRoute()
27
+ const sync = useSync()
28
+ const project = useProject()
29
+ const keybind = useKeybind()
30
+ const { theme } = useTheme()
31
+ const sdk = useSDK()
32
+ const toast = useToast()
33
+ const [toDelete, setToDelete] = createSignal<string>()
34
+ const [search, setSearch] = createDebouncedSignal("", 150)
35
+ const [global, setGlobal] = createSignal(true)
36
+ // TODO: extend /experimental/session to accept `scope`/`path` so this dialog can respect the
37
+ // upstream `session_directory_filter_enabled` KV toggle (via sync.session.query()) while
38
+ // keeping worktree grouping. Currently the toggle has no effect here.
39
+ const [searchResults, searchActions] = createResource(
40
+ () => search(),
41
+ async (query) => {
42
+ const result = await sdk.client.experimental.session.list(
43
+ {
44
+ search: query || undefined,
45
+ roots: true,
46
+ worktrees: true,
47
+ limit: 30,
48
+ },
49
+ { throwOnError: true },
50
+ )
51
+ return result.data ?? []
52
+ },
53
+ )
54
+
55
+ const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
56
+ const sessions = createMemo(() => {
57
+ const all = searchResults() ?? []
58
+ if (global()) return all
59
+ const root = project.instance.path().worktree
60
+ if (!root || root === "/") return all
61
+ return all.filter((s) => s.directory === root || s.directory.startsWith(root + path.sep))
62
+ })
63
+
64
+ function createWorkspace() {
65
+ dialog.replace(() => (
66
+ <DialogWorkspaceCreate
67
+ onSelect={(workspaceID) =>
68
+ openWorkspaceSession({
69
+ dialog,
70
+ route,
71
+ sdk,
72
+ sync,
73
+ toast,
74
+ workspaceID,
75
+ })
76
+ }
77
+ />
78
+ ))
79
+ }
80
+
81
+ function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
82
+ const workspace = project.workspace.get(session.workspaceID!)
83
+ const list = () => dialog.replace(() => <DialogSessionList />)
84
+ dialog.replace(() => (
85
+ <DialogSessionDeleteFailed
86
+ session={session.title}
87
+ workspace={workspace?.name ?? session.workspaceID!}
88
+ onDone={list}
89
+ onDelete={async () => {
90
+ const current = currentSessionID()
91
+ const info = current ? sync.data.session.find((item) => item.id === current) : undefined
92
+ const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
93
+ if (result.error) {
94
+ toast.show({
95
+ variant: "error",
96
+ title: "Failed to delete workspace",
97
+ message: errorMessage(result.error),
98
+ })
99
+ return false
100
+ }
101
+ await project.workspace.sync()
102
+ await sync.session.refresh()
103
+ if (search()) await searchActions.refetch()
104
+ if (info?.workspaceID === session.workspaceID) {
105
+ route.navigate({ type: "home" })
106
+ }
107
+ return true
108
+ }}
109
+ onRestore={() => {
110
+ dialog.replace(() => (
111
+ <DialogWorkspaceCreate
112
+ onSelect={(workspaceID) =>
113
+ restoreWorkspaceSession({
114
+ dialog,
115
+ sdk,
116
+ sync,
117
+ project,
118
+ toast,
119
+ workspaceID,
120
+ sessionID: session.id,
121
+ done: list,
122
+ })
123
+ }
124
+ />
125
+ ))
126
+ return false
127
+ }}
128
+ />
129
+ ))
130
+ }
131
+
132
+ const options = createMemo(() => {
133
+ const today = new Date().toDateString()
134
+ const all = global()
135
+ return sessions()
136
+ .filter((x) => x.parentID === undefined)
137
+ .toSorted((a, b) => {
138
+ const updatedDay = new Date(b.time.updated).setHours(0, 0, 0, 0) - new Date(a.time.updated).setHours(0, 0, 0, 0)
139
+ if (updatedDay !== 0) return updatedDay
140
+ return b.time.created - a.time.created
141
+ })
142
+ .map((x) => {
143
+ const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
144
+
145
+ let workspaceStatus: WorkspaceStatus | null = null
146
+ if (x.workspaceID) {
147
+ workspaceStatus = project.workspace.status(x.workspaceID) || "error"
148
+ }
149
+
150
+ let footer = ""
151
+ if (Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
152
+ if (x.workspaceID) {
153
+ let desc = "unknown"
154
+ if (workspace) {
155
+ desc = `${workspace.type}: ${workspace.name}`
156
+ }
157
+
158
+ footer = (
159
+ <>
160
+ {desc}{" "}
161
+ <span
162
+ style={{
163
+ fg: workspaceStatus === "connected" ? theme.success : theme.error,
164
+ }}
165
+ >
166
+
167
+ </span>
168
+ </>
169
+ )
170
+ }
171
+ } else {
172
+ footer = Locale.time(x.time.updated)
173
+ }
174
+
175
+ const date = new Date(x.time.updated)
176
+ let category = date.toDateString()
177
+ if (category === today) {
178
+ category = "Today"
179
+ }
180
+ const isDeleting = toDelete() === x.id
181
+ const status = sync.data.session_status?.[x.id]
182
+ const isWorking = status?.type === "busy"
183
+ return {
184
+ title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
185
+ description: all && x.worktreeName ? `(${x.worktreeName})` : undefined,
186
+ bg: isDeleting ? theme.error : undefined,
187
+ value: x.id,
188
+ category,
189
+ footer,
190
+ gutter: isWorking ? () => <Spinner /> : undefined,
191
+ }
192
+ })
193
+ })
194
+
195
+ onMount(() => {
196
+ dialog.setSize("large")
197
+ })
198
+
199
+ return (
200
+ <DialogSelect
201
+ title={global() ? "Sessions (all worktrees)" : "Sessions (current worktree)"}
202
+ options={options()}
203
+ skipFilter={true}
204
+ current={currentSessionID()}
205
+ onFilter={setSearch}
206
+ onMove={() => {
207
+ setToDelete(undefined)
208
+ }}
209
+ onSelect={(option) => {
210
+ route.navigate({
211
+ type: "session",
212
+ sessionID: option.value,
213
+ })
214
+ dialog.clear()
215
+ }}
216
+ keybind={[
217
+ {
218
+ keybind: keybind.all.session_delete?.[0],
219
+ title: "delete",
220
+ onTrigger: async (option) => {
221
+ if (toDelete() === option.value) {
222
+ const session = sessions().find((item) => item.id === option.value)
223
+ const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
224
+
225
+ try {
226
+ const result = await sdk.client.session.delete({
227
+ sessionID: option.value,
228
+ })
229
+ if (result.error) {
230
+ if (session?.workspaceID) {
231
+ recover(session)
232
+ } else {
233
+ toast.show({
234
+ variant: "error",
235
+ title: "Failed to delete session",
236
+ message: errorMessage(result.error),
237
+ })
238
+ }
239
+ setToDelete(undefined)
240
+ return
241
+ }
242
+ } catch (err) {
243
+ if (session?.workspaceID) {
244
+ recover(session)
245
+ } else {
246
+ toast.show({
247
+ variant: "error",
248
+ title: "Failed to delete session",
249
+ message: errorMessage(err),
250
+ })
251
+ }
252
+ setToDelete(undefined)
253
+ return
254
+ }
255
+ if (status && status !== "connected") {
256
+ await sync.session.refresh()
257
+ }
258
+ void searchActions.refetch()
259
+ setToDelete(undefined)
260
+ return
261
+ }
262
+ setToDelete(option.value)
263
+ },
264
+ },
265
+ {
266
+ keybind: keybind.all.session_rename?.[0],
267
+ title: "rename",
268
+ onTrigger: async (option) => {
269
+ const item = sessions().find((x) => x.id === option.value)
270
+ dialog.replace(() => (
271
+ <DialogSessionRename
272
+ session={option.value}
273
+ title={item?.title}
274
+ onConfirm={() => {
275
+ void searchActions.refetch()
276
+ }}
277
+ />
278
+ ))
279
+ },
280
+ },
281
+ {
282
+ keybind: { name: "a", ctrl: true, meta: false, shift: false, leader: false },
283
+ title: global() ? "current" : "all",
284
+ onTrigger: async () => {
285
+ setToDelete(undefined)
286
+ setGlobal((v) => !v)
287
+ },
288
+ },
289
+ {
290
+ keybind: Keybind.parse("ctrl+w")[0],
291
+ title: "new workspace",
292
+ side: "right",
293
+ disabled: !Flag.SAEEOL_EXPERIMENTAL_WORKSPACES,
294
+ onTrigger: () => {
295
+ createWorkspace()
296
+ },
297
+ },
298
+ ]}
299
+ />
300
+ )
301
+ }
@@ -0,0 +1,35 @@
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
+ title?: string
10
+ onConfirm?: () => void
11
+ }
12
+
13
+ export function DialogSessionRename(props: DialogSessionRenameProps) {
14
+ const dialog = useDialog()
15
+ const sync = useSync()
16
+ const sdk = useSDK()
17
+ const session = createMemo(() => sync.session.get(props.session))
18
+
19
+ return (
20
+ <DialogPrompt
21
+ title="Rename Session"
22
+ value={session()?.title ?? props.title}
23
+ onConfirm={(value) => {
24
+ void sdk.client.session
25
+ .update({
26
+ sessionID: props.session,
27
+ title: value,
28
+ })
29
+ .then(() => props.onConfirm?.())
30
+ dialog.clear()
31
+ }}
32
+ onCancel={() => dialog.clear()}
33
+ />
34
+ )
35
+ }
@@ -0,0 +1,37 @@
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
+ import { t } from "@/util/i18n"
6
+
7
+ export type DialogSkillProps = {
8
+ onSelect: (skill: string) => void
9
+ }
10
+
11
+ export function DialogSkill(props: DialogSkillProps) {
12
+ const dialog = useDialog()
13
+ const sdk = useSDK()
14
+ dialog.setSize("large")
15
+
16
+ const [skills] = createResource(async () => {
17
+ const result = await sdk.client.app.skills()
18
+ return result.data ?? []
19
+ })
20
+
21
+ const options = createMemo<DialogSelectOption<string>[]>(() => {
22
+ const list = skills() ?? []
23
+ const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
24
+ return list.map((skill) => ({
25
+ title: skill.name.padEnd(maxWidth),
26
+ description: skill.description?.replace(/\s+/g, " ").trim(),
27
+ value: skill.name,
28
+ category: t("cmd.skill.title"),
29
+ onSelect: () => {
30
+ props.onSelect(skill.name)
31
+ dialog.clear()
32
+ },
33
+ }))
34
+ })
35
+
36
+ return <DialogSelect title={t("cmd.skill.title")} placeholder={t("cmd.skill.search")} options={options()} />
37
+ }
@@ -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 { useKeybind } from "../../context/keybind"
7
+ import { usePromptStash, type StashEntry } from "../prompt/stash"
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
+ const keybind = useKeybind()
34
+
35
+ const [toDelete, setToDelete] = createSignal<number>()
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 ${keybind.print("stash_delete")} 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
+ keybind={[
72
+ {
73
+ keybind: keybind.all.stash_delete?.[0],
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
+ }
@@ -0,0 +1,190 @@
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 { useProject } from "@tui/context/project"
7
+ import { For, Match, Switch, Show, createMemo } from "solid-js"
8
+ import { InstallationVersion } from "@saeeol/core/installation/version"
9
+ import { Global } from "@saeeol/core/global"
10
+
11
+ export type DialogStatusProps = {}
12
+
13
+ export function DialogStatus() {
14
+ const sync = useSync()
15
+ const project = useProject()
16
+ const { theme } = useTheme()
17
+ const dialog = useDialog()
18
+
19
+ const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
20
+
21
+ const plugins = createMemo(() => {
22
+ const list = sync.data.config.plugin ?? []
23
+ const result = list.map((item) => {
24
+ const value = typeof item === "string" ? item : item[0]
25
+ if (value.startsWith("file://")) {
26
+ const path = fileURLToPath(value)
27
+ const parts = path.split(/[/\\]/)
28
+ const filename = parts.pop() || path
29
+ if (!filename.includes(".")) return { name: filename }
30
+ const basename = filename.split(".")[0]
31
+ if (basename === "index") {
32
+ const dirname = parts.pop()
33
+ const name = dirname || basename
34
+ return { name }
35
+ }
36
+ return { name: basename }
37
+ }
38
+ const index = value.lastIndexOf("@")
39
+ if (index <= 0) return { name: value, version: "latest" }
40
+ const name = value.substring(0, index)
41
+ const version = value.substring(index + 1)
42
+ return { name, version }
43
+ })
44
+ return result.toSorted((a, b) => a.name.localeCompare(b.name))
45
+ })
46
+
47
+ return (
48
+ <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
49
+ <box flexDirection="row" justifyContent="space-between">
50
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
51
+ Status
52
+ </text>
53
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
54
+ esc
55
+ </text>
56
+ </box>
57
+
58
+ <text fg={theme.textMuted}>Saeeol v{InstallationVersion}</text>
59
+
60
+
61
+ <box>
62
+ <text fg={theme.text}>Paths</text>
63
+ <text fg={theme.textMuted}>
64
+ Global config {" "}
65
+ {Global.Path.config.replace(Global.Path.home, "~")}
66
+ </text>
67
+ <Show when={project.instance.path().directory}>
68
+ <text fg={theme.textMuted}>
69
+ Project {" "}
70
+ {project.instance.path().directory.replace(Global.Path.home, "~")}
71
+ </text>
72
+ </Show>
73
+ </box>
74
+
75
+ <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
76
+ <box>
77
+ <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
78
+ <For each={Object.entries(sync.data.mcp)}>
79
+ {([key, item]) => (
80
+ <box flexDirection="row" gap={1}>
81
+ <text
82
+ flexShrink={0}
83
+ style={{
84
+ fg: (
85
+ {
86
+ connected: theme.success,
87
+ failed: theme.error,
88
+ disabled: theme.textMuted,
89
+ needs_auth: theme.warning,
90
+ needs_client_registration: theme.error,
91
+ } as Record<string, typeof theme.success>
92
+ )[item.status],
93
+ }}
94
+ >
95
+
96
+ </text>
97
+ <text fg={theme.text} wrapMode="word">
98
+ <b>{key}</b>{" "}
99
+ <span style={{ fg: theme.textMuted }}>
100
+ <Switch fallback={item.status}>
101
+ <Match when={item.status === "connected"}>Connected</Match>
102
+ <Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
103
+ <Match when={item.status === "disabled"}>Disabled in configuration</Match>
104
+ <Match when={(item.status as string) === "needs_auth"}>
105
+ Needs authentication (run: saeeol mcp auth {key})
106
+ </Match>
107
+ <Match when={(item.status as string) === "needs_client_registration" && item}>
108
+ {(val) => (val() as { error: string }).error}
109
+ </Match>
110
+ </Switch>
111
+ </span>
112
+ </text>
113
+ </box>
114
+ )}
115
+ </For>
116
+ </box>
117
+ </Show>
118
+ {sync.data.lsp.length > 0 && (
119
+ <box>
120
+ <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
121
+ <For each={sync.data.lsp}>
122
+ {(item) => (
123
+ <box flexDirection="row" gap={1}>
124
+ <text
125
+ flexShrink={0}
126
+ style={{
127
+ fg: {
128
+ connected: theme.success,
129
+ error: theme.error,
130
+ }[item.status],
131
+ }}
132
+ >
133
+
134
+ </text>
135
+ <text fg={theme.text} wrapMode="word">
136
+ <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
137
+ </text>
138
+ </box>
139
+ )}
140
+ </For>
141
+ </box>
142
+ )}
143
+ <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
144
+ <box>
145
+ <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
146
+ <For each={enabledFormatters()}>
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
+ </text>
160
+ </box>
161
+ )}
162
+ </For>
163
+ </box>
164
+ </Show>
165
+ <Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
166
+ <box>
167
+ <text fg={theme.text}>{plugins().length} Plugins</text>
168
+ <For each={plugins()}>
169
+ {(item) => (
170
+ <box flexDirection="row" gap={1}>
171
+ <text
172
+ flexShrink={0}
173
+ style={{
174
+ fg: theme.success,
175
+ }}
176
+ >
177
+
178
+ </text>
179
+ <text wrapMode="word" fg={theme.text}>
180
+ <b>{item.name}</b>
181
+ {item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
182
+ </text>
183
+ </box>
184
+ )}
185
+ </For>
186
+ </box>
187
+ </Show>
188
+ </box>
189
+ )
190
+ }
@@ -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
+ }