@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,60 @@
|
|
|
1
|
+
import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
|
|
3
|
+
import { isRecord } from "@/util/record"
|
|
4
|
+
|
|
5
|
+
type RuntimeSlotMap = TuiSlotMap<Record<string, object>>
|
|
6
|
+
|
|
7
|
+
type Slot = <Name extends string>(props: TuiSlotProps<Name>) => JSX.Element | null
|
|
8
|
+
export type HostSlotPlugin<Slots extends Record<string, object> = {}> = SolidPlugin<TuiSlotMap<Slots>, TuiSlotContext>
|
|
9
|
+
|
|
10
|
+
export type HostPluginApi = TuiPluginApi
|
|
11
|
+
export type HostSlots = {
|
|
12
|
+
register: {
|
|
13
|
+
(plugin: HostSlotPlugin): () => void
|
|
14
|
+
<Slots extends Record<string, object>>(plugin: HostSlotPlugin<Slots>): () => void
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function empty<Name extends string>(_props: TuiSlotProps<Name>) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let view: Slot = empty
|
|
23
|
+
|
|
24
|
+
export const Slot: Slot = (props) => view(props)
|
|
25
|
+
|
|
26
|
+
function isHostSlotPlugin(value: unknown): value is HostSlotPlugin<Record<string, object>> {
|
|
27
|
+
if (!isRecord(value)) return false
|
|
28
|
+
if (typeof value.id !== "string") return false
|
|
29
|
+
if (!isRecord(value.slots)) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setupSlots(api: HostPluginApi): HostSlots {
|
|
34
|
+
const reg = createSolidSlotRegistry<RuntimeSlotMap, TuiSlotContext>(
|
|
35
|
+
api.renderer,
|
|
36
|
+
{
|
|
37
|
+
theme: api.theme,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
onPluginError(event) {
|
|
41
|
+
console.error("[tui.slot] plugin error", {
|
|
42
|
+
plugin: event.pluginId,
|
|
43
|
+
slot: event.slot,
|
|
44
|
+
phase: event.phase,
|
|
45
|
+
source: event.source,
|
|
46
|
+
message: event.error.message,
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const slot = createSlot<RuntimeSlotMap, TuiSlotContext>(reg)
|
|
53
|
+
view = (props) => slot(props)
|
|
54
|
+
return {
|
|
55
|
+
register(plugin: HostSlotPlugin) {
|
|
56
|
+
if (!isHostSlotPlugin(plugin)) return () => {}
|
|
57
|
+
return reg.register(plugin)
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
|
2
|
+
import { createEffect, createSignal, onMount } from "solid-js"
|
|
3
|
+
import { Logo } from "../component/logo"
|
|
4
|
+
import { useProject } from "../context/project"
|
|
5
|
+
import { useSync } from "../context/sync"
|
|
6
|
+
import { Toast } from "../ui/toast"
|
|
7
|
+
import { useArgs } from "../context/args"
|
|
8
|
+
import { useRouteData } from "@tui/context/route"
|
|
9
|
+
import { usePromptRef } from "../context/prompt"
|
|
10
|
+
import { useLocal } from "../context/local"
|
|
11
|
+
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
|
12
|
+
import { useEditorContext } from "@tui/context/editor"
|
|
13
|
+
|
|
14
|
+
let once = false
|
|
15
|
+
const placeholder = {
|
|
16
|
+
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
|
|
17
|
+
shell: ["ls -la", "git status", "pwd"],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Home() {
|
|
21
|
+
const sync = useSync()
|
|
22
|
+
const project = useProject()
|
|
23
|
+
const route = useRouteData("home")
|
|
24
|
+
const promptRef = usePromptRef()
|
|
25
|
+
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
|
26
|
+
const args = useArgs()
|
|
27
|
+
const local = useLocal()
|
|
28
|
+
const editor = useEditorContext()
|
|
29
|
+
let sent = false
|
|
30
|
+
|
|
31
|
+
onMount(() => {
|
|
32
|
+
editor.clearSelection()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const bind = (r: PromptRef | undefined) => {
|
|
36
|
+
setRef(r)
|
|
37
|
+
promptRef.set(r)
|
|
38
|
+
if (once || !r) return
|
|
39
|
+
if (route.prompt) {
|
|
40
|
+
r.set(route.prompt)
|
|
41
|
+
once = true
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if (!args.prompt) return
|
|
45
|
+
r.set({ input: args.prompt, parts: [] })
|
|
46
|
+
once = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Wait for sync and model store to be ready before auto-submitting --prompt
|
|
50
|
+
createEffect(() => {
|
|
51
|
+
const r = ref()
|
|
52
|
+
if (sent) return
|
|
53
|
+
if (!r) return
|
|
54
|
+
if (!sync.ready || !local.model.ready) return
|
|
55
|
+
if (!args.prompt) return
|
|
56
|
+
if (r.current.input !== args.prompt) return
|
|
57
|
+
sent = true
|
|
58
|
+
r.submit()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
|
|
64
|
+
<box flexGrow={1} minHeight={0} />
|
|
65
|
+
<box height={4} minHeight={0} flexShrink={1} />
|
|
66
|
+
<box flexShrink={0}>
|
|
67
|
+
<TuiPluginRuntime.Slot name="home_logo" mode="replace">
|
|
68
|
+
<Logo />
|
|
69
|
+
</TuiPluginRuntime.Slot>
|
|
70
|
+
</box>
|
|
71
|
+
<box height={1} minHeight={0} flexShrink={1} />
|
|
72
|
+
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
|
|
73
|
+
<TuiPluginRuntime.Slot
|
|
74
|
+
name="home_prompt"
|
|
75
|
+
mode="replace"
|
|
76
|
+
workspace_id={project.workspace.current()}
|
|
77
|
+
ref={bind}
|
|
78
|
+
>
|
|
79
|
+
<Prompt
|
|
80
|
+
ref={bind}
|
|
81
|
+
workspaceID={project.workspace.current()}
|
|
82
|
+
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
|
|
83
|
+
placeholders={placeholder}
|
|
84
|
+
/>
|
|
85
|
+
</TuiPluginRuntime.Slot>
|
|
86
|
+
</box>
|
|
87
|
+
<TuiPluginRuntime.Slot name="home_bottom" />
|
|
88
|
+
<box flexGrow={1} minHeight={0} />
|
|
89
|
+
<Toast />
|
|
90
|
+
</box>
|
|
91
|
+
<box width="100%" flexShrink={0}>
|
|
92
|
+
<TuiPluginRuntime.Slot name="home_footer" mode="single_winner" />
|
|
93
|
+
</box>
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createMemo, onMount } from "solid-js"
|
|
2
|
+
import { useSync } from "@tui/context/sync"
|
|
3
|
+
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
|
4
|
+
import type { TextPart } from "@opencode-ai/sdk/v2"
|
|
5
|
+
import { Locale } from "@/util/locale"
|
|
6
|
+
import { useSDK } from "@tui/context/sdk"
|
|
7
|
+
import { useRoute } from "@tui/context/route"
|
|
8
|
+
import { useDialog, type DialogContext } from "../../ui/dialog"
|
|
9
|
+
import type { PromptInfo } from "@tui/component/prompt/history"
|
|
10
|
+
import { strip } from "@tui/component/prompt/part"
|
|
11
|
+
|
|
12
|
+
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
|
|
13
|
+
const sync = useSync()
|
|
14
|
+
const dialog = useDialog()
|
|
15
|
+
const sdk = useSDK()
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
|
|
18
|
+
onMount(() => {
|
|
19
|
+
dialog.setSize("large")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const options = createMemo((): DialogSelectOption<string | undefined>[] => {
|
|
23
|
+
const messages = sync.data.message[props.sessionID] ?? []
|
|
24
|
+
const fullSession = {
|
|
25
|
+
title: "Full session",
|
|
26
|
+
value: undefined,
|
|
27
|
+
onSelect: async (dialog: DialogContext) => {
|
|
28
|
+
const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
|
|
29
|
+
route.navigate({
|
|
30
|
+
sessionID: forked.data!.id,
|
|
31
|
+
type: "session",
|
|
32
|
+
})
|
|
33
|
+
dialog.clear()
|
|
34
|
+
},
|
|
35
|
+
} satisfies DialogSelectOption<string | undefined>
|
|
36
|
+
const result = [] as DialogSelectOption<string | undefined>[]
|
|
37
|
+
for (const message of messages) {
|
|
38
|
+
if (message.role !== "user") continue
|
|
39
|
+
const part = (sync.data.part[message.id] ?? []).find(
|
|
40
|
+
(x) => x.type === "text" && !x.synthetic && !x.ignored,
|
|
41
|
+
) as TextPart
|
|
42
|
+
if (!part) continue
|
|
43
|
+
result.push({
|
|
44
|
+
title: part.text.replace(/\n/g, " "),
|
|
45
|
+
value: message.id,
|
|
46
|
+
footer: Locale.time(message.time.created),
|
|
47
|
+
onSelect: async (dialog) => {
|
|
48
|
+
const forked = await sdk.client.session.fork({
|
|
49
|
+
sessionID: props.sessionID,
|
|
50
|
+
messageID: message.id,
|
|
51
|
+
})
|
|
52
|
+
const parts = sync.data.part[message.id] ?? []
|
|
53
|
+
const prompt = parts.reduce(
|
|
54
|
+
(agg, part) => {
|
|
55
|
+
if (part.type === "text") {
|
|
56
|
+
if (!part.synthetic) agg.input += part.text
|
|
57
|
+
}
|
|
58
|
+
if (part.type === "file") agg.parts.push(strip(part))
|
|
59
|
+
return agg
|
|
60
|
+
},
|
|
61
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
62
|
+
)
|
|
63
|
+
route.navigate({
|
|
64
|
+
sessionID: forked.data!.id,
|
|
65
|
+
type: "session",
|
|
66
|
+
prompt,
|
|
67
|
+
})
|
|
68
|
+
dialog.clear()
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return [fullSession, ...result.reverse()]
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
|
|
76
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createMemo } from "solid-js"
|
|
2
|
+
import { useSync } from "@tui/context/sync"
|
|
3
|
+
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
4
|
+
import { useSDK } from "@tui/context/sdk"
|
|
5
|
+
import { useRoute } from "@tui/context/route"
|
|
6
|
+
import * as Clipboard from "@tui/util/clipboard"
|
|
7
|
+
import type { PromptInfo } from "@tui/component/prompt/history"
|
|
8
|
+
import { strip } from "@tui/component/prompt/part"
|
|
9
|
+
|
|
10
|
+
export function DialogMessage(props: {
|
|
11
|
+
messageID: string
|
|
12
|
+
sessionID: string
|
|
13
|
+
setPrompt?: (prompt: PromptInfo) => void
|
|
14
|
+
}) {
|
|
15
|
+
const sync = useSync()
|
|
16
|
+
const sdk = useSDK()
|
|
17
|
+
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
|
|
18
|
+
const route = useRoute()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<DialogSelect
|
|
22
|
+
title="Message Actions"
|
|
23
|
+
options={[
|
|
24
|
+
{
|
|
25
|
+
title: "Revert",
|
|
26
|
+
value: "session.revert",
|
|
27
|
+
description: "undo messages and file changes",
|
|
28
|
+
onSelect: (dialog) => {
|
|
29
|
+
const msg = message()
|
|
30
|
+
if (!msg) return
|
|
31
|
+
|
|
32
|
+
void sdk.client.session.revert({
|
|
33
|
+
sessionID: props.sessionID,
|
|
34
|
+
messageID: msg.id,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (props.setPrompt) {
|
|
38
|
+
const parts = sync.data.part[msg.id]
|
|
39
|
+
const promptInfo = parts.reduce(
|
|
40
|
+
(agg, part) => {
|
|
41
|
+
if (part.type === "text") {
|
|
42
|
+
if (!part.synthetic) agg.input += part.text
|
|
43
|
+
}
|
|
44
|
+
if (part.type === "file") agg.parts.push(strip(part))
|
|
45
|
+
return agg
|
|
46
|
+
},
|
|
47
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
48
|
+
)
|
|
49
|
+
props.setPrompt(promptInfo)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dialog.clear()
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: "Copy",
|
|
57
|
+
value: "message.copy",
|
|
58
|
+
description: "message text to clipboard",
|
|
59
|
+
onSelect: async (dialog) => {
|
|
60
|
+
const msg = message()
|
|
61
|
+
if (!msg) return
|
|
62
|
+
|
|
63
|
+
const parts = sync.data.part[msg.id]
|
|
64
|
+
const text = parts.reduce((agg, part) => {
|
|
65
|
+
if (part.type === "text" && !part.synthetic) {
|
|
66
|
+
agg += part.text
|
|
67
|
+
}
|
|
68
|
+
return agg
|
|
69
|
+
}, "")
|
|
70
|
+
|
|
71
|
+
await Clipboard.copy(text)
|
|
72
|
+
dialog.clear()
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: "Fork",
|
|
77
|
+
value: "session.fork",
|
|
78
|
+
description: "create a new session",
|
|
79
|
+
onSelect: async (dialog) => {
|
|
80
|
+
const result = await sdk.client.session.fork({
|
|
81
|
+
sessionID: props.sessionID,
|
|
82
|
+
messageID: props.messageID,
|
|
83
|
+
})
|
|
84
|
+
const msg = message()
|
|
85
|
+
const prompt = msg
|
|
86
|
+
? sync.data.part[msg.id].reduce(
|
|
87
|
+
(agg, part) => {
|
|
88
|
+
if (part.type === "text") {
|
|
89
|
+
if (!part.synthetic) agg.input += part.text
|
|
90
|
+
}
|
|
91
|
+
if (part.type === "file") agg.parts.push(part)
|
|
92
|
+
return agg
|
|
93
|
+
},
|
|
94
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
95
|
+
)
|
|
96
|
+
: undefined
|
|
97
|
+
route.navigate({
|
|
98
|
+
sessionID: result.data!.id,
|
|
99
|
+
type: "session",
|
|
100
|
+
prompt,
|
|
101
|
+
})
|
|
102
|
+
dialog.clear()
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
2
|
+
import { useRoute } from "@tui/context/route"
|
|
3
|
+
|
|
4
|
+
export function DialogSubagent(props: { sessionID: string }) {
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<DialogSelect
|
|
9
|
+
title="Subagent Actions"
|
|
10
|
+
options={[
|
|
11
|
+
{
|
|
12
|
+
title: "Open",
|
|
13
|
+
value: "subagent.view",
|
|
14
|
+
description: "the subagent's session",
|
|
15
|
+
onSelect: (dialog) => {
|
|
16
|
+
route.navigate({
|
|
17
|
+
type: "session",
|
|
18
|
+
sessionID: props.sessionID,
|
|
19
|
+
})
|
|
20
|
+
dialog.clear()
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createMemo, onMount } from "solid-js"
|
|
2
|
+
import { useSync } from "@tui/context/sync"
|
|
3
|
+
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
|
4
|
+
import type { TextPart } from "@opencode-ai/sdk/v2"
|
|
5
|
+
import { Locale } from "@/util/locale"
|
|
6
|
+
import { DialogMessage } from "./dialog-message"
|
|
7
|
+
import { useDialog } from "../../ui/dialog"
|
|
8
|
+
import type { PromptInfo } from "../../component/prompt/history"
|
|
9
|
+
|
|
10
|
+
export function DialogTimeline(props: {
|
|
11
|
+
sessionID: string
|
|
12
|
+
onMove: (messageID: string) => void
|
|
13
|
+
setPrompt?: (prompt: PromptInfo) => void
|
|
14
|
+
}) {
|
|
15
|
+
const sync = useSync()
|
|
16
|
+
const dialog = useDialog()
|
|
17
|
+
|
|
18
|
+
onMount(() => {
|
|
19
|
+
dialog.setSize("large")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const options = createMemo((): DialogSelectOption<string>[] => {
|
|
23
|
+
const messages = sync.data.message[props.sessionID] ?? []
|
|
24
|
+
const result = [] as DialogSelectOption<string>[]
|
|
25
|
+
for (const message of messages) {
|
|
26
|
+
if (message.role !== "user") continue
|
|
27
|
+
const part = (sync.data.part[message.id] ?? []).find(
|
|
28
|
+
(x) => x.type === "text" && !x.synthetic && !x.ignored,
|
|
29
|
+
) as TextPart
|
|
30
|
+
if (!part) continue
|
|
31
|
+
result.push({
|
|
32
|
+
title: part.text.replace(/\n/g, " "),
|
|
33
|
+
value: message.id,
|
|
34
|
+
footer: Locale.time(message.time.created),
|
|
35
|
+
onSelect: (dialog) => {
|
|
36
|
+
dialog.replace(() => (
|
|
37
|
+
<DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
|
|
38
|
+
))
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
result.reverse()
|
|
43
|
+
return result
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Timeline" options={options()} />
|
|
47
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
2
|
+
import { useTheme } from "../../context/theme"
|
|
3
|
+
import { useSync } from "../../context/sync"
|
|
4
|
+
import { useDirectory } from "../../context/directory"
|
|
5
|
+
import { useConnected } from "../../component/use-connected"
|
|
6
|
+
import { createStore } from "solid-js/store"
|
|
7
|
+
import { useRoute } from "../../context/route"
|
|
8
|
+
|
|
9
|
+
export function Footer() {
|
|
10
|
+
const { theme } = useTheme()
|
|
11
|
+
const sync = useSync()
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
|
|
14
|
+
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
|
|
15
|
+
const lsp = createMemo(() => Object.keys(sync.data.lsp))
|
|
16
|
+
const permissions = createMemo(() => {
|
|
17
|
+
if (route.data.type !== "session") return []
|
|
18
|
+
return sync.data.permission[route.data.sessionID] ?? []
|
|
19
|
+
})
|
|
20
|
+
const directory = useDirectory()
|
|
21
|
+
const connected = useConnected()
|
|
22
|
+
|
|
23
|
+
const [store, setStore] = createStore({
|
|
24
|
+
welcome: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
onMount(() => {
|
|
28
|
+
// Track all timeouts to ensure proper cleanup
|
|
29
|
+
const timeouts: ReturnType<typeof setTimeout>[] = []
|
|
30
|
+
|
|
31
|
+
function tick() {
|
|
32
|
+
if (connected()) return
|
|
33
|
+
if (!store.welcome) {
|
|
34
|
+
setStore("welcome", true)
|
|
35
|
+
timeouts.push(setTimeout(() => tick(), 5000))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (store.welcome) {
|
|
40
|
+
setStore("welcome", false)
|
|
41
|
+
timeouts.push(setTimeout(() => tick(), 10_000))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
timeouts.push(setTimeout(() => tick(), 10_000))
|
|
46
|
+
|
|
47
|
+
onCleanup(() => {
|
|
48
|
+
timeouts.forEach(clearTimeout)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
|
|
54
|
+
<text fg={theme.textMuted}>{directory()}</text>
|
|
55
|
+
<box gap={2} flexDirection="row" flexShrink={0}>
|
|
56
|
+
<Switch>
|
|
57
|
+
<Match when={store.welcome}>
|
|
58
|
+
<text fg={theme.text}>
|
|
59
|
+
Get started <span style={{ fg: theme.textMuted }}>/connect</span>
|
|
60
|
+
</text>
|
|
61
|
+
</Match>
|
|
62
|
+
<Match when={connected()}>
|
|
63
|
+
<Show when={permissions().length > 0}>
|
|
64
|
+
<text fg={theme.warning}>
|
|
65
|
+
<span style={{ fg: theme.warning }}>△</span> {permissions().length} Permission
|
|
66
|
+
{permissions().length > 1 ? "s" : ""}
|
|
67
|
+
</text>
|
|
68
|
+
</Show>
|
|
69
|
+
<text fg={theme.text}>
|
|
70
|
+
<span style={{ fg: lsp().length > 0 ? theme.success : theme.textMuted }}>•</span> {lsp().length} LSP
|
|
71
|
+
</text>
|
|
72
|
+
<Show when={mcp()}>
|
|
73
|
+
<text fg={theme.text}>
|
|
74
|
+
<Switch>
|
|
75
|
+
<Match when={mcpError()}>
|
|
76
|
+
<span style={{ fg: theme.error }}>⊙ </span>
|
|
77
|
+
</Match>
|
|
78
|
+
<Match when={true}>
|
|
79
|
+
<span style={{ fg: theme.success }}>⊙ </span>
|
|
80
|
+
</Match>
|
|
81
|
+
</Switch>
|
|
82
|
+
{mcp()} MCP
|
|
83
|
+
</text>
|
|
84
|
+
</Show>
|
|
85
|
+
<text fg={theme.textMuted}>/status</text>
|
|
86
|
+
</Match>
|
|
87
|
+
</Switch>
|
|
88
|
+
</box>
|
|
89
|
+
</box>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APX session chat view.
|
|
3
|
+
*
|
|
4
|
+
* A self-contained chat interface that streams messages through the APX daemon
|
|
5
|
+
* using the APX sync context. The complex opencode session view is replaced with
|
|
6
|
+
* a simple but functional chat layout.
|
|
7
|
+
*/
|
|
8
|
+
import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
|
9
|
+
import { TextareaRenderable } from "@opentui/core"
|
|
10
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
11
|
+
import { useTheme } from "@tui/context/theme"
|
|
12
|
+
import { useRoute } from "@tui/context/route"
|
|
13
|
+
import { useApxSync } from "@tui/context/sync-apx"
|
|
14
|
+
import { useToast, Toast } from "@tui/ui/toast"
|
|
15
|
+
import { useExit } from "@tui/context/exit"
|
|
16
|
+
import { usePromptRef } from "@tui/context/prompt"
|
|
17
|
+
import type { ApxMessage } from "@tui/context/sync-apx"
|
|
18
|
+
|
|
19
|
+
function UserBubble(props: { msg: ApxMessage }) {
|
|
20
|
+
const { theme } = useTheme()
|
|
21
|
+
return (
|
|
22
|
+
<box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
|
|
23
|
+
<text color={theme.primary} bold>
|
|
24
|
+
You
|
|
25
|
+
</text>
|
|
26
|
+
<text color={theme.text} wrap>
|
|
27
|
+
{props.msg.text}
|
|
28
|
+
</text>
|
|
29
|
+
</box>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function AssistantBubble(props: { msg: ApxMessage }) {
|
|
34
|
+
const { theme } = useTheme()
|
|
35
|
+
const hasText = () => props.msg.text.length > 0
|
|
36
|
+
return (
|
|
37
|
+
<box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
|
|
38
|
+
<text color={theme.success} bold>
|
|
39
|
+
{props.msg.streaming ? "Assistant ▸" : "Assistant"}
|
|
40
|
+
</text>
|
|
41
|
+
<Show when={hasText()} fallback={<text color={theme.textMuted}>…</text>}>
|
|
42
|
+
<text color={props.msg.error ? theme.error : theme.text} wrap>
|
|
43
|
+
{props.msg.text}
|
|
44
|
+
</text>
|
|
45
|
+
</Show>
|
|
46
|
+
</box>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Session() {
|
|
51
|
+
const dims = useTerminalDimensions()
|
|
52
|
+
const { theme } = useTheme()
|
|
53
|
+
const route = useRoute()
|
|
54
|
+
const sync = useApxSync()
|
|
55
|
+
const toast = useToast()
|
|
56
|
+
const exit = useExit()
|
|
57
|
+
const promptRef = usePromptRef()
|
|
58
|
+
const [sending, setSending] = createSignal(false)
|
|
59
|
+
let inputEl: TextareaRenderable | undefined
|
|
60
|
+
|
|
61
|
+
const sessionID = createMemo(() => {
|
|
62
|
+
if (route.data.type === "session") return route.data.sessionID
|
|
63
|
+
return sync.session.current() ?? ""
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const messages = createMemo(() => sync.session.messages(sessionID()))
|
|
67
|
+
|
|
68
|
+
onCleanup(() => {
|
|
69
|
+
promptRef.set(undefined)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
function makeRef(r: TextareaRenderable) {
|
|
73
|
+
return {
|
|
74
|
+
get focused() {
|
|
75
|
+
return r.focused
|
|
76
|
+
},
|
|
77
|
+
get current() {
|
|
78
|
+
return { input: r.plainText, parts: [] as any[] }
|
|
79
|
+
},
|
|
80
|
+
set(prompt: { input: string; parts: any[] }) {
|
|
81
|
+
r.setText(prompt.input)
|
|
82
|
+
},
|
|
83
|
+
reset() {
|
|
84
|
+
r.clear()
|
|
85
|
+
},
|
|
86
|
+
blur() {
|
|
87
|
+
r.blur()
|
|
88
|
+
},
|
|
89
|
+
focus() {
|
|
90
|
+
r.focus()
|
|
91
|
+
},
|
|
92
|
+
submit() {
|
|
93
|
+
void handleSubmit()
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleSubmit() {
|
|
99
|
+
if (!inputEl) return
|
|
100
|
+
const text = inputEl.plainText.trim()
|
|
101
|
+
if (!text || sending()) return
|
|
102
|
+
|
|
103
|
+
// Check for exit commands
|
|
104
|
+
if (text === "exit" || text === "quit" || text === ":q") {
|
|
105
|
+
void exit()
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
inputEl.clear()
|
|
110
|
+
setSending(true)
|
|
111
|
+
try {
|
|
112
|
+
await sync.sendMessage(text)
|
|
113
|
+
} catch (e) {
|
|
114
|
+
toast.error(e instanceof Error ? e : new Error(String(e)))
|
|
115
|
+
} finally {
|
|
116
|
+
setSending(false)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<box flexDirection="column" flexGrow={1} width={dims().width} height={dims().height}>
|
|
122
|
+
{/* Message list */}
|
|
123
|
+
<scrollbox
|
|
124
|
+
flexGrow={1}
|
|
125
|
+
stickyScroll
|
|
126
|
+
stickyStart="bottom"
|
|
127
|
+
verticalScrollbarOptions={{ visible: true }}
|
|
128
|
+
>
|
|
129
|
+
<box flexDirection="column" width={dims().width}>
|
|
130
|
+
<Show
|
|
131
|
+
when={messages().length > 0}
|
|
132
|
+
fallback={
|
|
133
|
+
<box paddingLeft={2} paddingTop={2}>
|
|
134
|
+
<text color={theme.textMuted} italic>
|
|
135
|
+
Type a message and press Enter to start chatting.
|
|
136
|
+
</text>
|
|
137
|
+
</box>
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
<For each={messages()}>
|
|
141
|
+
{(msg) => (
|
|
142
|
+
<Show when={msg.role === "user"} fallback={<AssistantBubble msg={msg} />}>
|
|
143
|
+
<UserBubble msg={msg} />
|
|
144
|
+
</Show>
|
|
145
|
+
)}
|
|
146
|
+
</For>
|
|
147
|
+
</Show>
|
|
148
|
+
<box height={1} />
|
|
149
|
+
</box>
|
|
150
|
+
</scrollbox>
|
|
151
|
+
|
|
152
|
+
{/* Input area */}
|
|
153
|
+
<box
|
|
154
|
+
flexShrink={0}
|
|
155
|
+
flexDirection="column"
|
|
156
|
+
borderTop={1}
|
|
157
|
+
borderColor={theme.border}
|
|
158
|
+
backgroundColor={theme.backgroundElement}
|
|
159
|
+
>
|
|
160
|
+
<box paddingLeft={2} paddingRight={2} paddingTop={1}>
|
|
161
|
+
<textarea
|
|
162
|
+
ref={(r: TextareaRenderable) => {
|
|
163
|
+
inputEl = r
|
|
164
|
+
promptRef.set(makeRef(r))
|
|
165
|
+
}}
|
|
166
|
+
placeholder={sending() ? "Waiting for response…" : "Ask anything... (Enter to send)"}
|
|
167
|
+
placeholderColor={theme.textMuted}
|
|
168
|
+
textColor={theme.text}
|
|
169
|
+
focusedTextColor={theme.text}
|
|
170
|
+
minHeight={1}
|
|
171
|
+
maxHeight={6}
|
|
172
|
+
onSubmit={() => {
|
|
173
|
+
setTimeout(() => setTimeout(() => handleSubmit(), 0), 0)
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</box>
|
|
177
|
+
<box height={1} paddingLeft={2} paddingRight={2}>
|
|
178
|
+
<Show when={sending()}>
|
|
179
|
+
<text color={theme.textMuted} italic>
|
|
180
|
+
Streaming…
|
|
181
|
+
</text>
|
|
182
|
+
</Show>
|
|
183
|
+
</box>
|
|
184
|
+
</box>
|
|
185
|
+
<Toast />
|
|
186
|
+
</box>
|
|
187
|
+
)
|
|
188
|
+
}
|