@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.
- package/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate Promise.allSettled results into a single Error that names every
|
|
3
|
+
* failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
|
|
4
|
+
* boundaries so a single 4xx doesn't drown its parallel siblings as
|
|
5
|
+
* unhandled rejections — every failure surfaces in one labeled message.
|
|
6
|
+
*/
|
|
7
|
+
export type LabeledSettled = {
|
|
8
|
+
name: string
|
|
9
|
+
result: PromiseSettledResult<unknown>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
|
|
13
|
+
const failed = labeled.filter(
|
|
14
|
+
(x): x is { name: string; result: PromiseRejectedResult } => x.result.status === "rejected",
|
|
15
|
+
)
|
|
16
|
+
if (failed.length === 0) return null
|
|
17
|
+
|
|
18
|
+
const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ")
|
|
19
|
+
const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}`
|
|
20
|
+
const err = new Error(summary)
|
|
21
|
+
err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) }
|
|
22
|
+
return err
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reasonMessage(reason: unknown): string {
|
|
26
|
+
if (reason instanceof Error) return reason.message
|
|
27
|
+
if (typeof reason === "string") return reason
|
|
28
|
+
if (reason && typeof reason === "object") {
|
|
29
|
+
const obj = reason as { message?: unknown; name?: unknown }
|
|
30
|
+
if (typeof obj.message === "string") return obj.message
|
|
31
|
+
if (typeof obj.name === "string") return obj.name
|
|
32
|
+
}
|
|
33
|
+
return String(reason)
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createSimpleContext } from "./helper"
|
|
2
|
+
|
|
3
|
+
export interface Args {
|
|
4
|
+
model?: string
|
|
5
|
+
agent?: string
|
|
6
|
+
prompt?: string
|
|
7
|
+
continue?: boolean
|
|
8
|
+
sessionID?: string
|
|
9
|
+
fork?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
|
|
13
|
+
name: "Args",
|
|
14
|
+
init: (props: Args) => props,
|
|
15
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js"
|
|
2
|
+
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
|
3
|
+
import { useDialog, type DialogContext } from "@tui/ui/dialog"
|
|
4
|
+
import {
|
|
5
|
+
formatKeyBindings,
|
|
6
|
+
reactiveMatcherFromSignal,
|
|
7
|
+
type OpenTuiKeymap,
|
|
8
|
+
useKeymapSelector,
|
|
9
|
+
useOpencodeKeymap,
|
|
10
|
+
} from "../keymap"
|
|
11
|
+
import { useTuiConfig } from "./tui-config"
|
|
12
|
+
|
|
13
|
+
type SlashEntry = {
|
|
14
|
+
display: string
|
|
15
|
+
description?: string
|
|
16
|
+
aliases?: string[]
|
|
17
|
+
onSelect: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CommandPaletteContext = {
|
|
21
|
+
run(command: string): void
|
|
22
|
+
show(): void
|
|
23
|
+
slashes: Accessor<readonly SlashEntry[]>
|
|
24
|
+
suspend(enabled: boolean): void
|
|
25
|
+
readonly suspended: boolean
|
|
26
|
+
matcher: ReturnType<typeof reactiveMatcherFromSignal>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COMMAND_PALETTE_DIALOG = "command.palette.show"
|
|
30
|
+
const ctx = createContext<CommandPaletteContext>()
|
|
31
|
+
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
|
|
32
|
+
|
|
33
|
+
function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
|
|
34
|
+
return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_DIALOG
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
|
|
38
|
+
const suggested = entry.command.suggested
|
|
39
|
+
if (typeof suggested === "boolean") return suggested
|
|
40
|
+
if (typeof suggested === "function") return suggested() === true
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function CommandPaletteProvider(props: ParentProps) {
|
|
45
|
+
const dialog = useDialog()
|
|
46
|
+
const keymap = useOpencodeKeymap()
|
|
47
|
+
const [suspendCount, setSuspendCount] = createSignal(0)
|
|
48
|
+
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
|
|
49
|
+
keymap
|
|
50
|
+
.getCommandEntries({
|
|
51
|
+
visibility: "reachable",
|
|
52
|
+
namespace: "palette",
|
|
53
|
+
})
|
|
54
|
+
.filter(isVisiblePaletteCommand),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const run = (command: string) => {
|
|
58
|
+
keymap.dispatchCommand(command)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const slashes = createMemo<SlashEntry[]>(() =>
|
|
62
|
+
entries().flatMap((entry) => {
|
|
63
|
+
const slashName = entry.command.slashName
|
|
64
|
+
if (typeof slashName !== "string" || !slashName) return []
|
|
65
|
+
const slashAliases = entry.command.slashAliases
|
|
66
|
+
return {
|
|
67
|
+
display: `/${slashName}`,
|
|
68
|
+
description:
|
|
69
|
+
typeof entry.command.desc === "string"
|
|
70
|
+
? entry.command.desc
|
|
71
|
+
: typeof entry.command.title === "string"
|
|
72
|
+
? entry.command.title
|
|
73
|
+
: undefined,
|
|
74
|
+
aliases: Array.isArray(slashAliases)
|
|
75
|
+
? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
|
|
76
|
+
: undefined,
|
|
77
|
+
onSelect: () => run(entry.command.name),
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const value: CommandPaletteContext = {
|
|
83
|
+
run,
|
|
84
|
+
show() {
|
|
85
|
+
dialog.replace(() => <CommandPaletteDialog run={run} />)
|
|
86
|
+
},
|
|
87
|
+
slashes,
|
|
88
|
+
suspend(enabled: boolean) {
|
|
89
|
+
setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1)))
|
|
90
|
+
},
|
|
91
|
+
get suspended() {
|
|
92
|
+
return suspendCount() > 0 || dialog.stack.length > 0
|
|
93
|
+
},
|
|
94
|
+
matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function useCommandPalette() {
|
|
101
|
+
const value = useContext(ctx)
|
|
102
|
+
if (!value) throw new Error("CommandPalette context must be used within a CommandPaletteProvider")
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function CommandPaletteDialog(props: { run(command: string): void }) {
|
|
107
|
+
const config = useTuiConfig()
|
|
108
|
+
const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
|
|
109
|
+
const query = {
|
|
110
|
+
namespace: "palette",
|
|
111
|
+
}
|
|
112
|
+
const reachable = keymap
|
|
113
|
+
.getCommandEntries({
|
|
114
|
+
...query,
|
|
115
|
+
visibility: "reachable",
|
|
116
|
+
})
|
|
117
|
+
.filter(isVisiblePaletteCommand)
|
|
118
|
+
const registeredBindings = keymap.getCommandBindings({
|
|
119
|
+
visibility: "registered",
|
|
120
|
+
commands: reachable.map((entry) => entry.command.name),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return reachable.map((entry) => ({
|
|
124
|
+
...entry,
|
|
125
|
+
bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
|
|
126
|
+
}))
|
|
127
|
+
})
|
|
128
|
+
const options = createMemo(() =>
|
|
129
|
+
entries().map((entry) => ({
|
|
130
|
+
title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
|
|
131
|
+
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
|
|
132
|
+
category: typeof entry.command.category === "string" ? entry.command.category : undefined,
|
|
133
|
+
footer: formatKeyBindings(entry.bindings, config),
|
|
134
|
+
value: entry.command.name,
|
|
135
|
+
suggested: isSuggestedPaletteCommand(entry),
|
|
136
|
+
onSelect: (dialog: DialogContext) => {
|
|
137
|
+
dialog.clear()
|
|
138
|
+
props.run(entry.command.name)
|
|
139
|
+
},
|
|
140
|
+
})),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
let ref: DialogSelectRef<string>
|
|
144
|
+
const list = () => {
|
|
145
|
+
if (ref?.filter) return options()
|
|
146
|
+
return [
|
|
147
|
+
...options()
|
|
148
|
+
.filter((option) => option.suggested)
|
|
149
|
+
.map((option) => ({
|
|
150
|
+
...option,
|
|
151
|
+
value: `suggested:${option.value}`,
|
|
152
|
+
category: "Suggested",
|
|
153
|
+
})),
|
|
154
|
+
...options(),
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function useCommandSlashes(): Accessor<readonly SlashEntry[]> {
|
|
162
|
+
return useCommandPalette().slashes
|
|
163
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createMemo } from "solid-js"
|
|
2
|
+
import { useProject } from "./project"
|
|
3
|
+
import { useSync } from "./sync"
|
|
4
|
+
import { Global } from "@opencode-ai/core/global"
|
|
5
|
+
|
|
6
|
+
export function useDirectory() {
|
|
7
|
+
const project = useProject()
|
|
8
|
+
const sync = useSync()
|
|
9
|
+
return createMemo(() => {
|
|
10
|
+
const directory = project.instance.path().directory || process.cwd()
|
|
11
|
+
const result = directory.replace(Global.Path.home, "~")
|
|
12
|
+
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
|
13
|
+
return result
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { Option, Schema } from "effect"
|
|
5
|
+
import { Filesystem } from "@/util/filesystem"
|
|
6
|
+
import type { EditorSelection } from "./editor"
|
|
7
|
+
|
|
8
|
+
const ZedEditorRowSchema = Schema.Struct({
|
|
9
|
+
item_kind: Schema.String,
|
|
10
|
+
editor_id: Schema.NullOr(Schema.Number),
|
|
11
|
+
workspace_id: Schema.Number,
|
|
12
|
+
workspace_paths: Schema.NullOr(Schema.String),
|
|
13
|
+
timestamp: Schema.String,
|
|
14
|
+
buffer_path: Schema.NullOr(Schema.String),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const ZedSelectionRowSchema = Schema.Struct({
|
|
18
|
+
selection_start: Schema.NullOr(Schema.Number),
|
|
19
|
+
selection_end: Schema.NullOr(Schema.Number),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const ZedEditorContentsSchema = Schema.Struct({
|
|
23
|
+
contents: Schema.NullOr(Schema.String),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)
|
|
27
|
+
const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)
|
|
28
|
+
const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
|
|
29
|
+
|
|
30
|
+
const utf8 = new TextEncoder()
|
|
31
|
+
|
|
32
|
+
type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>
|
|
33
|
+
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
|
34
|
+
|
|
35
|
+
export type ZedSelectionResult =
|
|
36
|
+
| { type: "selection"; selection: EditorSelection }
|
|
37
|
+
| { type: "empty" }
|
|
38
|
+
| { type: "unavailable" }
|
|
39
|
+
|
|
40
|
+
export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> {
|
|
41
|
+
const active = queryZedActiveEditor(dbPath, cwd)
|
|
42
|
+
if (active.type !== "row") return active
|
|
43
|
+
|
|
44
|
+
const row = active.row
|
|
45
|
+
if (!row.buffer_path) return { type: "empty" }
|
|
46
|
+
|
|
47
|
+
const selections = queryZedEditorSelections(dbPath, row)
|
|
48
|
+
if (selections.type !== "selections") return selections
|
|
49
|
+
const byteRanges = selections.selections
|
|
50
|
+
.flatMap((selection) => {
|
|
51
|
+
if (selection.selection_start == null || selection.selection_end == null) return []
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
start: Math.min(selection.selection_start, selection.selection_end),
|
|
55
|
+
end: Math.max(selection.selection_start, selection.selection_end),
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
.sort((left, right) => left.start - right.start || left.end - right.end)
|
|
60
|
+
if (byteRanges.length === 0) return { type: "unavailable" }
|
|
61
|
+
|
|
62
|
+
const contents = queryZedEditorContents(dbPath, row)
|
|
63
|
+
const text =
|
|
64
|
+
contents.type === "contents" && contents.contents != null
|
|
65
|
+
? contents.contents
|
|
66
|
+
: await Bun.file(row.buffer_path)
|
|
67
|
+
.text()
|
|
68
|
+
.catch(() => undefined)
|
|
69
|
+
if (text == null) return { type: "unavailable" }
|
|
70
|
+
|
|
71
|
+
const ranges = byteRanges.map((range) => {
|
|
72
|
+
const startOffset = utf8ByteOffsetToStringIndex(text, range.start)
|
|
73
|
+
const endOffset = utf8ByteOffsetToStringIndex(text, range.end)
|
|
74
|
+
return {
|
|
75
|
+
text: text.slice(startOffset, endOffset),
|
|
76
|
+
selection: offsetsToSelection(text, startOffset, endOffset),
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
type: "selection",
|
|
82
|
+
selection: {
|
|
83
|
+
filePath: row.buffer_path,
|
|
84
|
+
source: "zed",
|
|
85
|
+
ranges,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function queryZedActiveEditor(dbPath: string, cwd: string) {
|
|
91
|
+
let db: Database | undefined
|
|
92
|
+
try {
|
|
93
|
+
db = new Database(dbPath, { readonly: true })
|
|
94
|
+
const raw = db
|
|
95
|
+
.query(
|
|
96
|
+
`select
|
|
97
|
+
i.kind as item_kind,
|
|
98
|
+
e.item_id as editor_id,
|
|
99
|
+
i.workspace_id as workspace_id,
|
|
100
|
+
w.paths as workspace_paths,
|
|
101
|
+
w.timestamp as timestamp,
|
|
102
|
+
e.buffer_path as buffer_path
|
|
103
|
+
from items i
|
|
104
|
+
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
|
|
105
|
+
join workspaces w on w.workspace_id = i.workspace_id
|
|
106
|
+
left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
|
|
107
|
+
where i.active = 1 and p.active = 1
|
|
108
|
+
order by w.timestamp desc`,
|
|
109
|
+
)
|
|
110
|
+
.all()
|
|
111
|
+
|
|
112
|
+
const rows = raw.flatMap((row) => {
|
|
113
|
+
const parsed = decodeZedEditorRow(row)
|
|
114
|
+
return Option.isSome(parsed) ? [parsed.value] : []
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
|
|
118
|
+
|
|
119
|
+
const row = rows
|
|
120
|
+
.map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) }))
|
|
121
|
+
.filter((entry) => entry.score > 0)
|
|
122
|
+
.sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
|
|
123
|
+
if (!row) return { type: "empty" as const }
|
|
124
|
+
if (row.item_kind !== "Editor") return { type: "unavailable" as const }
|
|
125
|
+
if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
|
|
126
|
+
return { type: "row" as const, row }
|
|
127
|
+
} catch {
|
|
128
|
+
return { type: "unavailable" as const }
|
|
129
|
+
} finally {
|
|
130
|
+
db?.close()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
|
|
135
|
+
let db: Database | undefined
|
|
136
|
+
try {
|
|
137
|
+
db = new Database(dbPath, { readonly: true })
|
|
138
|
+
const raw = db
|
|
139
|
+
.query(
|
|
140
|
+
`select
|
|
141
|
+
start as selection_start,
|
|
142
|
+
end as selection_end
|
|
143
|
+
from editor_selections
|
|
144
|
+
where editor_id = $editorID and workspace_id = $workspaceID`,
|
|
145
|
+
)
|
|
146
|
+
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
|
|
147
|
+
|
|
148
|
+
const selections = raw.flatMap((selection) => {
|
|
149
|
+
const parsed = decodeZedSelectionRow(selection)
|
|
150
|
+
return Option.isSome(parsed) ? [parsed.value] : []
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
|
|
154
|
+
return { type: "selections" as const, selections }
|
|
155
|
+
} catch {
|
|
156
|
+
return { type: "unavailable" as const }
|
|
157
|
+
} finally {
|
|
158
|
+
db?.close()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
|
|
163
|
+
let db: Database | undefined
|
|
164
|
+
try {
|
|
165
|
+
db = new Database(dbPath, { readonly: true })
|
|
166
|
+
const parsed = decodeZedEditorContents(
|
|
167
|
+
db
|
|
168
|
+
.query(
|
|
169
|
+
`select contents
|
|
170
|
+
from editors
|
|
171
|
+
where item_id = $editorID and workspace_id = $workspaceID`,
|
|
172
|
+
)
|
|
173
|
+
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
|
|
174
|
+
)
|
|
175
|
+
if (Option.isNone(parsed)) return { type: "unavailable" as const }
|
|
176
|
+
return { type: "contents" as const, contents: parsed.value.contents }
|
|
177
|
+
} catch {
|
|
178
|
+
return { type: "unavailable" as const }
|
|
179
|
+
} finally {
|
|
180
|
+
db?.close()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
|
|
185
|
+
return row.item_kind === "Editor" && row.editor_id != null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveZedDbPath() {
|
|
189
|
+
const candidates = [
|
|
190
|
+
process.env.OPENCODE_ZED_DB,
|
|
191
|
+
path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
|
|
192
|
+
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
|
|
193
|
+
].filter((item): item is string => Boolean(item))
|
|
194
|
+
|
|
195
|
+
return candidates.find((item) => isFile(item))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isFile(item: string) {
|
|
199
|
+
try {
|
|
200
|
+
return Filesystem.stat(item)?.isFile() === true
|
|
201
|
+
} catch {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
|
|
207
|
+
return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
|
|
208
|
+
if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
|
|
209
|
+
return score
|
|
210
|
+
}, 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function zedWorkspacePaths(value: string | null) {
|
|
214
|
+
if (!value) return []
|
|
215
|
+
const parsed = parseJson(value)
|
|
216
|
+
if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string")
|
|
217
|
+
return value.split(/\r?\n/).filter(Boolean)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function offsetToPosition(text: string, offset: number) {
|
|
221
|
+
const stringOffset = utf8ByteOffsetToStringIndex(text, offset)
|
|
222
|
+
return offsetsToSelection(text, stringOffset, stringOffset).start
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) {
|
|
226
|
+
if (byteOffset <= 0) return 0
|
|
227
|
+
|
|
228
|
+
let bytes = 0
|
|
229
|
+
for (let index = 0; index < text.length; ) {
|
|
230
|
+
const codePoint = text.codePointAt(index)
|
|
231
|
+
if (codePoint === undefined) return text.length
|
|
232
|
+
|
|
233
|
+
const nextIndex = index + (codePoint > 0xffff ? 2 : 1)
|
|
234
|
+
bytes += utf8.encode(text.slice(index, nextIndex)).length
|
|
235
|
+
if (bytes >= byteOffset) return nextIndex
|
|
236
|
+
index = nextIndex
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return text.length
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function offsetsToSelection(text: string, startOffset: number, endOffset: number) {
|
|
243
|
+
const start = Math.max(0, Math.min(startOffset, text.length))
|
|
244
|
+
const end = Math.max(0, Math.min(endOffset, text.length))
|
|
245
|
+
let line = 1
|
|
246
|
+
let lineStart = 0
|
|
247
|
+
let startPosition = position(line, lineStart, start)
|
|
248
|
+
let endPosition = position(line, lineStart, end)
|
|
249
|
+
|
|
250
|
+
for (let index = 0; index <= end; index++) {
|
|
251
|
+
if (index === start) startPosition = position(line, lineStart, index)
|
|
252
|
+
if (index === end) {
|
|
253
|
+
endPosition = position(line, lineStart, index)
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
if (text[index] === "\n") {
|
|
257
|
+
line += 1
|
|
258
|
+
lineStart = index + 1
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { start: startPosition, end: endPosition }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function position(line: number, lineStart: number, offset: number) {
|
|
266
|
+
return {
|
|
267
|
+
line,
|
|
268
|
+
character: offset - lineStart + 1,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pathContains(parent: string, child: string) {
|
|
273
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
|
274
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseJson(value: string) {
|
|
278
|
+
try {
|
|
279
|
+
return JSON.parse(value) as unknown
|
|
280
|
+
} catch {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
}
|