mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
sync
This commit is contained in:
parent
857c083e35
commit
e6d549497c
6 changed files with 105 additions and 91 deletions
|
|
@ -3,7 +3,9 @@
|
|||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
"options": {
|
||||
// "baseURL": "http://localhost:8080"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -20,21 +20,12 @@ import { useTheme } from "@tui/context/theme"
|
|||
import {
|
||||
BoxRenderable,
|
||||
ScrollBoxRenderable,
|
||||
TextAttributes,
|
||||
addDefaultParsers,
|
||||
MacOSScrollAccel,
|
||||
type ScrollAcceleration,
|
||||
} from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Part,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
CompactionPart,
|
||||
} from "@opencode-ai/sdk"
|
||||
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { Locale } from "@/util/locale"
|
||||
import type { Tool } from "@/tool/tool"
|
||||
|
|
@ -674,13 +665,6 @@ export function Session() {
|
|||
// snap to bottom when session changes
|
||||
createEffect(on(() => route.sessionID, toBottom))
|
||||
|
||||
const status = createMemo(
|
||||
() =>
|
||||
sync.data.session_status[route.sessionID] ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<context.Provider
|
||||
value={{
|
||||
|
|
@ -820,7 +804,7 @@ export function Session() {
|
|||
</Match>
|
||||
<Match when={message.role === "assistant"}>
|
||||
<AssistantMessage
|
||||
last={index() === messages().length - 1}
|
||||
last={pending() === message.id}
|
||||
message={message as AssistantMessage}
|
||||
parts={sync.data.part[message.id] ?? []}
|
||||
/>
|
||||
|
|
@ -829,17 +813,6 @@ export function Session() {
|
|||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<Show when={status().type !== "idle"}>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<text fg={local.agent.color(lastUserMessage().agent)}>{Locale.titlecase(lastUserMessage().agent)}</text>
|
||||
<Shimmer text={lastUserMessage().model.modelID} color={theme.text} />
|
||||
<Show when={status().type === "retry"}>
|
||||
<text fg={theme.error}>
|
||||
{(status() as any).message} [retry #{(status() as any).attempt}]
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<box flexShrink={0}>
|
||||
<Prompt
|
||||
ref={(r) => (prompt = r)}
|
||||
|
|
@ -957,6 +930,13 @@ function UserMessage(props: {
|
|||
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
|
||||
const local = useLocal()
|
||||
const { theme } = useTheme()
|
||||
const sync = useSync()
|
||||
const status = createMemo(
|
||||
() =>
|
||||
sync.data.session_status[props.message.sessionID] ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<For each={props.parts}>
|
||||
|
|
@ -974,9 +954,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
)
|
||||
}}
|
||||
</For>
|
||||
<Show
|
||||
when={props.message.error && (props.message.error.name !== "APIError" || !props.message.error.data.isRetryable)}
|
||||
>
|
||||
<Show when={props.message.error}>
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
|
|
@ -990,6 +968,17 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={props.last && status().type !== "idle"}>
|
||||
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
|
||||
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
||||
<Shimmer text={props.message.modelID} color={theme.text} />
|
||||
<Show when={status().type === "retry"}>
|
||||
<text fg={theme.error}>
|
||||
{(status() as any).message} [attempt #{(status() as any).attempt}]
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
props.message.time.completed &&
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { TuiEvent } from "@/cli/cmd/tui/event"
|
|||
import { Snapshot } from "@/snapshot"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
|
|
@ -376,7 +377,7 @@ export namespace Server {
|
|||
description: "Get session status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.record(z.string(), SessionPrompt.Status)),
|
||||
schema: resolver(z.record(z.string(), SessionStatus.Info)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -384,7 +385,7 @@ export namespace Server {
|
|||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const result = SessionPrompt.status()
|
||||
const result = SessionStatus.list()
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Snapshot } from "@/snapshot"
|
|||
import { SessionSummary } from "./summary"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionRetry } from "./retry"
|
||||
import { SessionStatus } from "./status"
|
||||
|
||||
export namespace SessionProcessor {
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
|
|
@ -49,6 +50,7 @@ export namespace SessionProcessor {
|
|||
input.abort.throwIfAborted()
|
||||
switch (value.type) {
|
||||
case "start":
|
||||
SessionStatus.set(input.sessionID, { type: "busy" })
|
||||
break
|
||||
|
||||
case "reasoning-start":
|
||||
|
|
@ -325,7 +327,12 @@ export namespace SessionProcessor {
|
|||
attempt++
|
||||
const delay = SessionRetry.getRetryDelayInMs(error, attempt)
|
||||
if (delay) {
|
||||
await SessionRetry.sleep(delay, input.abort)
|
||||
SessionStatus.set(input.sessionID, {
|
||||
type: "retry",
|
||||
attempt,
|
||||
message: error.data.message,
|
||||
})
|
||||
await SessionRetry.sleep(delay, input.abort).catch(() => {})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ import { Plugin } from "../plugin"
|
|||
|
||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { defer } from "../util/defer"
|
||||
import { mapValues, mergeDeep, pipe } from "remeda"
|
||||
import { mergeDeep, pipe } from "remeda"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { MCP } from "../mcp"
|
||||
|
|
@ -48,39 +47,13 @@ import { NamedError } from "@/util/error"
|
|||
import { fn } from "@/util/fn"
|
||||
import { SessionProcessor } from "./processor"
|
||||
import { TaskTool } from "@/tool/task"
|
||||
import type { Message } from "vscode-jsonrpc"
|
||||
import { SessionStatus } from "./status"
|
||||
|
||||
export namespace SessionPrompt {
|
||||
const log = Log.create({ service: "session.prompt" })
|
||||
export const OUTPUT_TOKEN_MAX = 32_000
|
||||
|
||||
export const Status = z
|
||||
.union([
|
||||
z.object({
|
||||
type: z.literal("idle"),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("retry"),
|
||||
attempt: z.number(),
|
||||
message: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("busy"),
|
||||
}),
|
||||
])
|
||||
.meta({
|
||||
ref: "SessionStatus",
|
||||
})
|
||||
export type Status = z.infer<typeof Status>
|
||||
|
||||
export const Event = {
|
||||
Status: Bus.event(
|
||||
"session.status",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
status: Status,
|
||||
}),
|
||||
),
|
||||
Idle: Bus.event(
|
||||
"session.idle",
|
||||
z.object({
|
||||
|
|
@ -95,7 +68,6 @@ export namespace SessionPrompt {
|
|||
string,
|
||||
{
|
||||
abort: AbortController
|
||||
status: Status
|
||||
callbacks: {
|
||||
resolve(input: MessageV2.WithParts): void
|
||||
reject(): void
|
||||
|
|
@ -111,21 +83,9 @@ export namespace SessionPrompt {
|
|||
},
|
||||
)
|
||||
|
||||
export function status() {
|
||||
return mapValues(state(), (item) => item.status)
|
||||
}
|
||||
|
||||
export function getStatus(sessionID: string) {
|
||||
return (
|
||||
state()[sessionID]?.status ?? {
|
||||
type: "idle",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function assertNotBusy(sessionID: string) {
|
||||
const status = getStatus(sessionID)
|
||||
if (status?.type !== "idle") throw new Session.BusyError(sessionID)
|
||||
const match = state()[sessionID]
|
||||
if (match) throw new Session.BusyError(sessionID)
|
||||
}
|
||||
|
||||
export const PromptInput = z.object({
|
||||
|
|
@ -252,13 +212,8 @@ export namespace SessionPrompt {
|
|||
const controller = new AbortController()
|
||||
s[sessionID] = {
|
||||
abort: controller,
|
||||
status: { type: "busy" },
|
||||
callbacks: [],
|
||||
}
|
||||
Bus.publish(Event.Status, {
|
||||
sessionID,
|
||||
status: s[sessionID].status,
|
||||
})
|
||||
return controller.signal
|
||||
}
|
||||
|
||||
|
|
@ -272,10 +227,7 @@ export namespace SessionPrompt {
|
|||
item.reject()
|
||||
}
|
||||
delete s[sessionID]
|
||||
Bus.publish(Event.Status, {
|
||||
sessionID,
|
||||
status: { type: "idle" },
|
||||
})
|
||||
SessionStatus.set(sessionID, { type: "idle" })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
63
packages/opencode/src/session/status.ts
Normal file
63
packages/opencode/src/session/status.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Bus } from "@/bus"
|
||||
import { Instance } from "@/project/instance"
|
||||
import z from "zod"
|
||||
|
||||
export namespace SessionStatus {
|
||||
export const Info = z
|
||||
.union([
|
||||
z.object({
|
||||
type: z.literal("idle"),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("retry"),
|
||||
attempt: z.number(),
|
||||
message: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("busy"),
|
||||
}),
|
||||
])
|
||||
.meta({
|
||||
ref: "SessionStatus",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Status: Bus.event(
|
||||
"session.status",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
status: Info,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
const state = Instance.state(() => {
|
||||
const data: Record<string, Info> = {}
|
||||
return data
|
||||
})
|
||||
|
||||
export function get(sessionID: string) {
|
||||
return (
|
||||
state()[sessionID] ?? {
|
||||
type: "idle",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function list() {
|
||||
return Object.values(state())
|
||||
}
|
||||
|
||||
export function set(sessionID: string, status: Info) {
|
||||
Bus.publish(Event.Status, {
|
||||
sessionID,
|
||||
status,
|
||||
})
|
||||
if (status.type === "idle") {
|
||||
delete state()[sessionID]
|
||||
return
|
||||
}
|
||||
state()[sessionID] = status
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue