|
|
@@ -1,55 +1,33 @@
|
|
|
import type {
|
|
|
- Message,
|
|
|
Agent,
|
|
|
- Provider,
|
|
|
- Session,
|
|
|
- Part,
|
|
|
- Config,
|
|
|
- Todo,
|
|
|
Command,
|
|
|
- PermissionRequest,
|
|
|
- QuestionRequest,
|
|
|
+ Config,
|
|
|
+ ConsoleState,
|
|
|
+ FormatterStatus,
|
|
|
LspStatus,
|
|
|
- McpStatus,
|
|
|
McpResource,
|
|
|
- FormatterStatus,
|
|
|
- SessionStatus,
|
|
|
- ProviderListResponse,
|
|
|
+ McpStatus,
|
|
|
+ Message,
|
|
|
+ Part,
|
|
|
+ PermissionRequest,
|
|
|
+ Provider,
|
|
|
ProviderAuthMethod,
|
|
|
- VcsInfo,
|
|
|
+ ProviderListResponse,
|
|
|
+ QuestionRequest,
|
|
|
+ Session,
|
|
|
SnapshotFileDiff,
|
|
|
- ConsoleState,
|
|
|
+ Todo,
|
|
|
+ VcsInfo,
|
|
|
} from "@opencode-ai/sdk/v2"
|
|
|
-import { createStore, produce, reconcile } from "solid-js/store"
|
|
|
-import { useProject } from "./project"
|
|
|
-import { useEvent } from "./event"
|
|
|
-import { useSDK } from "./sdk"
|
|
|
-import { useTuiStartup } from "./runtime"
|
|
|
+import { createStore } from "solid-js/store"
|
|
|
import { createSimpleContext } from "./helper"
|
|
|
-import { useExit } from "./exit"
|
|
|
-import { useArgs } from "./args"
|
|
|
-import { batch, onMount } from "solid-js"
|
|
|
-import path from "path"
|
|
|
-import { useKV } from "./kv"
|
|
|
+import { useProject } from "./project"
|
|
|
|
|
|
const emptyConsoleState: ConsoleState = {
|
|
|
consoleManagedProviders: [],
|
|
|
switchableOrgCount: 0,
|
|
|
}
|
|
|
|
|
|
-function search<T>(items: T[], target: string, key: (item: T) => string) {
|
|
|
- let left = 0
|
|
|
- let right = items.length - 1
|
|
|
- while (left <= right) {
|
|
|
- const middle = Math.floor((left + right) / 2)
|
|
|
- const value = key(items[middle])
|
|
|
- if (value === target) return { found: true, index: middle }
|
|
|
- if (value < target) left = middle + 1
|
|
|
- else right = middle - 1
|
|
|
- }
|
|
|
- return { found: false, index: left }
|
|
|
-}
|
|
|
-
|
|
|
export const {
|
|
|
context: SyncContext,
|
|
|
use: useSync,
|
|
|
@@ -57,8 +35,7 @@ export const {
|
|
|
} = createSimpleContext({
|
|
|
name: "Sync",
|
|
|
init: () => {
|
|
|
- const startup = useTuiStartup()
|
|
|
- const kv = useKV()
|
|
|
+ const project = useProject()
|
|
|
const [store, setStore] = createStore<{
|
|
|
status: "loading" | "partial" | "complete"
|
|
|
provider: Provider[]
|
|
|
@@ -71,39 +48,23 @@ export const {
|
|
|
provider_auth: Record<string, ProviderAuthMethod[]>
|
|
|
agent: Agent[]
|
|
|
command: Command[]
|
|
|
- permission: {
|
|
|
- [sessionID: string]: PermissionRequest[]
|
|
|
- }
|
|
|
- question: {
|
|
|
- [sessionID: string]: QuestionRequest[]
|
|
|
- }
|
|
|
+ permission: Record<string, PermissionRequest[]>
|
|
|
+ question: Record<string, QuestionRequest[]>
|
|
|
config: Config
|
|
|
session: Session[]
|
|
|
- session_status: {
|
|
|
- [sessionID: string]: SessionStatus
|
|
|
- }
|
|
|
- session_diff: {
|
|
|
- [sessionID: string]: SnapshotFileDiff[]
|
|
|
- }
|
|
|
- todo: {
|
|
|
- [sessionID: string]: Todo[]
|
|
|
- }
|
|
|
- message: {
|
|
|
- [sessionID: string]: Message[]
|
|
|
- }
|
|
|
- part: {
|
|
|
- [messageID: string]: Part[]
|
|
|
- }
|
|
|
+ session_diff: Record<string, SnapshotFileDiff[]>
|
|
|
+ todo: Record<string, Todo[]>
|
|
|
+ message: Record<string, Message[]>
|
|
|
+ part: Record<string, Part[]>
|
|
|
lsp: LspStatus[]
|
|
|
- mcp: {
|
|
|
- [key: string]: McpStatus
|
|
|
- }
|
|
|
- mcp_resource: {
|
|
|
- [key: string]: McpResource
|
|
|
- }
|
|
|
+ mcp: Record<string, McpStatus>
|
|
|
+ mcp_resource: Record<string, McpResource>
|
|
|
formatter: FormatterStatus[]
|
|
|
vcs: VcsInfo | undefined
|
|
|
}>({
|
|
|
+ status: "complete",
|
|
|
+ provider: [],
|
|
|
+ provider_default: {},
|
|
|
provider_next: {
|
|
|
all: [],
|
|
|
default: {},
|
|
|
@@ -114,16 +75,12 @@ export const {
|
|
|
experimentalBackgroundSubagents: false,
|
|
|
},
|
|
|
provider_auth: {},
|
|
|
- config: {},
|
|
|
- status: "loading",
|
|
|
agent: [],
|
|
|
+ command: [],
|
|
|
permission: {},
|
|
|
question: {},
|
|
|
- command: [],
|
|
|
- provider: [],
|
|
|
- provider_default: {},
|
|
|
+ config: {},
|
|
|
session: [],
|
|
|
- session_status: {},
|
|
|
session_diff: {},
|
|
|
todo: {},
|
|
|
message: {},
|
|
|
@@ -135,521 +92,32 @@ export const {
|
|
|
vcs: undefined,
|
|
|
})
|
|
|
|
|
|
- const event = useEvent()
|
|
|
- const project = useProject()
|
|
|
- const sdk = useSDK()
|
|
|
-
|
|
|
- const fullSyncedSessions = new Set<string>()
|
|
|
- const syncingSessions = new Map<string, Promise<void>>()
|
|
|
- const hydratingSessions = new Map<string, { messages: Set<string>; parts: Set<string> }>()
|
|
|
- const touchMessage = (sessionID: string, messageID: string) => {
|
|
|
- hydratingSessions.get(sessionID)?.messages.add(messageID)
|
|
|
- }
|
|
|
- const touchPart = (sessionID: string, partID: string) => {
|
|
|
- hydratingSessions.get(sessionID)?.parts.add(partID)
|
|
|
- }
|
|
|
-
|
|
|
- function sessionListQuery(): { scope?: "project"; path?: string } {
|
|
|
- if (!kv.get("session_directory_filter_enabled", true)) return { scope: "project" }
|
|
|
- if (!project.data.instance.path.worktree || !project.data.instance.path.directory) return { scope: "project" }
|
|
|
- return {
|
|
|
- path: path
|
|
|
- .relative(path.resolve(project.data.instance.path.worktree), project.data.instance.path.directory)
|
|
|
- .replaceAll("\\", "/"),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function listSessions() {
|
|
|
- return sdk.client.session
|
|
|
- .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() })
|
|
|
- .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
|
|
- }
|
|
|
-
|
|
|
- event.subscribe((event, { workspace }) => {
|
|
|
- switch (event.type) {
|
|
|
- case "server.instance.disposed":
|
|
|
- void bootstrap()
|
|
|
- break
|
|
|
- case "permission.replied": {
|
|
|
- const requests = store.permission[event.properties.sessionID]
|
|
|
- if (!requests) break
|
|
|
- const match = search(requests, event.properties.requestID, (r) => r.id)
|
|
|
- if (!match.found) break
|
|
|
- setStore(
|
|
|
- "permission",
|
|
|
- event.properties.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(match.index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "permission.asked": {
|
|
|
- const request = event.properties
|
|
|
- const requests = store.permission[request.sessionID]
|
|
|
- if (!requests) {
|
|
|
- setStore("permission", request.sessionID, [request])
|
|
|
- break
|
|
|
- }
|
|
|
- const match = search(requests, request.id, (r) => r.id)
|
|
|
- if (match.found) {
|
|
|
- setStore("permission", request.sessionID, match.index, reconcile(request))
|
|
|
- break
|
|
|
- }
|
|
|
- setStore(
|
|
|
- "permission",
|
|
|
- request.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(match.index, 0, request)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "question.replied":
|
|
|
- case "question.rejected": {
|
|
|
- const requests = store.question[event.properties.sessionID]
|
|
|
- if (!requests) break
|
|
|
- const match = search(requests, event.properties.requestID, (r) => r.id)
|
|
|
- if (!match.found) break
|
|
|
- setStore(
|
|
|
- "question",
|
|
|
- event.properties.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(match.index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "question.asked": {
|
|
|
- const request = event.properties
|
|
|
- const requests = store.question[request.sessionID]
|
|
|
- if (!requests) {
|
|
|
- setStore("question", request.sessionID, [request])
|
|
|
- break
|
|
|
- }
|
|
|
- const match = search(requests, request.id, (r) => r.id)
|
|
|
- if (match.found) {
|
|
|
- setStore("question", request.sessionID, match.index, reconcile(request))
|
|
|
- break
|
|
|
- }
|
|
|
- setStore(
|
|
|
- "question",
|
|
|
- request.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(match.index, 0, request)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "todo.updated":
|
|
|
- setStore("todo", event.properties.sessionID, event.properties.todos)
|
|
|
- break
|
|
|
-
|
|
|
- case "session.diff":
|
|
|
- setStore("session_diff", event.properties.sessionID, event.properties.diff)
|
|
|
- break
|
|
|
-
|
|
|
- case "session.deleted": {
|
|
|
- const result = search(store.session, event.properties.info.id, (s) => s.id)
|
|
|
- if (result.found) {
|
|
|
- setStore(
|
|
|
- "session",
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
- case "session.updated": {
|
|
|
- const result = search(store.session, event.properties.info.id, (s) => s.id)
|
|
|
- if (result.found) {
|
|
|
- setStore("session", result.index, reconcile(event.properties.info))
|
|
|
- break
|
|
|
- }
|
|
|
- setStore(
|
|
|
- "session",
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 0, event.properties.info)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "session.next.moved": {
|
|
|
- const result = search(store.session, event.properties.sessionID, (s) => s.id)
|
|
|
- if (!result.found) break
|
|
|
- setStore(
|
|
|
- "session",
|
|
|
- result.index,
|
|
|
- produce((session) => {
|
|
|
- session.directory = event.properties.location.directory
|
|
|
- session.path = event.properties.subdirectory
|
|
|
- session.workspaceID = event.properties.location.workspaceID
|
|
|
- session.time.updated = event.properties.timestamp
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "session.status": {
|
|
|
- setStore("session_status", event.properties.sessionID, event.properties.status)
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "message.updated": {
|
|
|
- touchMessage(event.properties.info.sessionID, event.properties.info.id)
|
|
|
- const messages = store.message[event.properties.info.sessionID]
|
|
|
- if (!messages) {
|
|
|
- setStore("message", event.properties.info.sessionID, [event.properties.info])
|
|
|
- break
|
|
|
- }
|
|
|
- const result = search(messages, event.properties.info.id, (m) => m.id)
|
|
|
- if (result.found) {
|
|
|
- setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
|
|
- break
|
|
|
- }
|
|
|
- setStore(
|
|
|
- "message",
|
|
|
- event.properties.info.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 0, event.properties.info)
|
|
|
- }),
|
|
|
- )
|
|
|
- const updated = store.message[event.properties.info.sessionID]
|
|
|
- if (updated.length > 100) {
|
|
|
- const oldest = updated[0]
|
|
|
- batch(() => {
|
|
|
- setStore(
|
|
|
- "message",
|
|
|
- event.properties.info.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.shift()
|
|
|
- }),
|
|
|
- )
|
|
|
- setStore(
|
|
|
- "part",
|
|
|
- produce((draft) => {
|
|
|
- delete draft[oldest.id]
|
|
|
- }),
|
|
|
- )
|
|
|
- })
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
- case "message.removed": {
|
|
|
- touchMessage(event.properties.sessionID, event.properties.messageID)
|
|
|
- const messages = store.message[event.properties.sessionID]
|
|
|
- const result = search(messages, event.properties.messageID, (m) => m.id)
|
|
|
- if (result.found) {
|
|
|
- setStore(
|
|
|
- "message",
|
|
|
- event.properties.sessionID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
- case "message.part.updated": {
|
|
|
- touchPart(event.properties.part.sessionID, event.properties.part.id)
|
|
|
- const parts = store.part[event.properties.part.messageID]
|
|
|
- if (!parts) {
|
|
|
- setStore("part", event.properties.part.messageID, [event.properties.part])
|
|
|
- break
|
|
|
- }
|
|
|
- const result = search(parts, event.properties.part.id, (p) => p.id)
|
|
|
- if (result.found) {
|
|
|
- setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
|
|
- break
|
|
|
- }
|
|
|
- setStore(
|
|
|
- "part",
|
|
|
- event.properties.part.messageID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 0, event.properties.part)
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "message.part.delta": {
|
|
|
- const parts = store.part[event.properties.messageID]
|
|
|
- if (!parts) break
|
|
|
- const result = search(parts, event.properties.partID, (p) => p.id)
|
|
|
- if (!result.found) break
|
|
|
- touchPart(event.properties.sessionID, event.properties.partID)
|
|
|
- setStore(
|
|
|
- "part",
|
|
|
- event.properties.messageID,
|
|
|
- produce((draft) => {
|
|
|
- const part = draft[result.index]
|
|
|
- const field = event.properties.field as keyof typeof part
|
|
|
- const existing = part[field] as string | undefined
|
|
|
- ;(part[field] as string) = (existing ?? "") + event.properties.delta
|
|
|
- }),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "message.part.removed": {
|
|
|
- touchPart(event.properties.sessionID, event.properties.partID)
|
|
|
- const parts = store.part[event.properties.messageID]
|
|
|
- const result = search(parts, event.properties.partID, (p) => p.id)
|
|
|
- if (result.found) {
|
|
|
- setStore(
|
|
|
- "part",
|
|
|
- event.properties.messageID,
|
|
|
- produce((draft) => {
|
|
|
- draft.splice(result.index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "lsp.updated": {
|
|
|
- const workspace = project.workspace.current()
|
|
|
- void sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? []))
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case "vcs.branch.updated": {
|
|
|
- if (workspace === project.workspace.current()) {
|
|
|
- setStore("vcs", { branch: event.properties.branch })
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- const exit = useExit()
|
|
|
- const args = useArgs()
|
|
|
-
|
|
|
- async function bootstrap(input: { fatal?: boolean } = {}) {
|
|
|
- const fatal = input.fatal ?? true
|
|
|
- const workspace = project.workspace.current()
|
|
|
- const projectPromise = project.sync()
|
|
|
- const sessionListPromise = projectPromise.then(() => listSessions())
|
|
|
-
|
|
|
- // blocking - include session.list when continuing a session
|
|
|
- const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
|
|
|
- const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
|
|
|
- const capabilitiesPromise = sdk.client.experimental.capabilities
|
|
|
- .get({ workspace }, { throwOnError: true })
|
|
|
- .then((x) => x.data)
|
|
|
- .catch(() => undefined)
|
|
|
- const consoleStatePromise = sdk.client.experimental.console
|
|
|
- .get({ workspace }, { throwOnError: true })
|
|
|
- .then((x) => x.data)
|
|
|
- .catch(() => emptyConsoleState)
|
|
|
- const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
|
|
|
- const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
|
|
|
- await Promise.all([
|
|
|
- providersPromise,
|
|
|
- providerListPromise,
|
|
|
- capabilitiesPromise,
|
|
|
- agentsPromise,
|
|
|
- configPromise,
|
|
|
- projectPromise,
|
|
|
- ...(args.continue ? [sessionListPromise] : []),
|
|
|
- ])
|
|
|
- .then(async () => {
|
|
|
- const providersResponse = providersPromise.then((x) => x.data!)
|
|
|
- const providerListResponse = providerListPromise.then((x) => x.data!)
|
|
|
- const capabilitiesResponse = capabilitiesPromise
|
|
|
- const consoleStateResponse = consoleStatePromise
|
|
|
- const agentsResponse = agentsPromise.then((x) => x.data ?? [])
|
|
|
- const configResponse = configPromise.then((x) => x.data!)
|
|
|
- const sessionListResponse = args.continue ? sessionListPromise : undefined
|
|
|
-
|
|
|
- return Promise.all([
|
|
|
- providersResponse,
|
|
|
- providerListResponse,
|
|
|
- capabilitiesResponse,
|
|
|
- consoleStateResponse,
|
|
|
- agentsResponse,
|
|
|
- configResponse,
|
|
|
- ...(sessionListResponse ? [sessionListResponse] : []),
|
|
|
- ]).then((responses) => {
|
|
|
- const providers = responses[0]
|
|
|
- const providerList = responses[1]
|
|
|
- const capabilities = responses[2]
|
|
|
- const consoleState = responses[3]
|
|
|
- const agents = responses[4]
|
|
|
- const config = responses[5]
|
|
|
- const sessions = responses[6]
|
|
|
-
|
|
|
- batch(() => {
|
|
|
- setStore("provider", reconcile(providers.providers))
|
|
|
- setStore("provider_default", reconcile(providers.default))
|
|
|
- setStore("provider_next", reconcile(providerList))
|
|
|
- setStore("capabilities", "experimentalBackgroundSubagents", capabilities?.backgroundSubagents === true)
|
|
|
- setStore("console_state", reconcile(consoleState))
|
|
|
- setStore("agent", reconcile(agents))
|
|
|
- setStore("config", reconcile(config))
|
|
|
- if (sessions !== undefined) setStore("session", reconcile(sessions))
|
|
|
- })
|
|
|
- })
|
|
|
- })
|
|
|
- .then(() => {
|
|
|
- if (store.status !== "complete") setStore("status", "partial")
|
|
|
- // non-blocking
|
|
|
- void Promise.all([
|
|
|
- ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
|
|
- consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
|
|
|
- sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
|
|
|
- sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data ?? []))),
|
|
|
- sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data ?? {}))),
|
|
|
- sdk.client.experimental.resource
|
|
|
- .list({ workspace })
|
|
|
- .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
|
|
- sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data ?? []))),
|
|
|
- sdk.client.session.status({ workspace }).then((x) => {
|
|
|
- setStore("session_status", reconcile(x.data ?? {}))
|
|
|
- }),
|
|
|
- sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
|
|
|
- sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
|
|
|
- project.workspace.sync(),
|
|
|
- ]).then(() => {
|
|
|
- setStore("status", "complete")
|
|
|
- })
|
|
|
- })
|
|
|
- .catch(async (e) => {
|
|
|
- console.error("tui bootstrap failed", {
|
|
|
- error: e instanceof Error ? e.message : String(e),
|
|
|
- name: e instanceof Error ? e.name : undefined,
|
|
|
- stack: e instanceof Error ? e.stack : undefined,
|
|
|
- })
|
|
|
- if (fatal) {
|
|
|
- exit(e)
|
|
|
- } else {
|
|
|
- throw e
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- onMount(() => {
|
|
|
- void bootstrap()
|
|
|
- })
|
|
|
-
|
|
|
- const result = {
|
|
|
+ return {
|
|
|
data: store,
|
|
|
set: setStore,
|
|
|
get status() {
|
|
|
return store.status
|
|
|
},
|
|
|
get ready() {
|
|
|
- if (startup.skipInitialLoading) return true
|
|
|
- return store.status !== "loading"
|
|
|
+ return true
|
|
|
},
|
|
|
get path() {
|
|
|
return project.instance.path()
|
|
|
},
|
|
|
session: {
|
|
|
- get(sessionID: string) {
|
|
|
- const match = search(store.session, sessionID, (s) => s.id)
|
|
|
- if (match.found) return store.session[match.index]
|
|
|
- return undefined
|
|
|
+ get(_sessionID: string) {
|
|
|
+ return undefined as Session | undefined
|
|
|
},
|
|
|
query() {
|
|
|
- return sessionListQuery()
|
|
|
- },
|
|
|
- async refresh() {
|
|
|
- const list = await listSessions()
|
|
|
- setStore("session", reconcile(list))
|
|
|
- },
|
|
|
- status(sessionID: string) {
|
|
|
- const session = result.session.get(sessionID)
|
|
|
- if (!session) return "idle"
|
|
|
- if (session.time.compacting) return "compacting"
|
|
|
- const messages = store.message[sessionID] ?? []
|
|
|
- const last = messages.at(-1)
|
|
|
- if (!last) return "idle"
|
|
|
- if (last.role === "user") return "working"
|
|
|
- return last.time.completed ? "idle" : "working"
|
|
|
+ return {} as { scope?: "project"; path?: string }
|
|
|
},
|
|
|
- async sync(sessionID: string) {
|
|
|
- if (fullSyncedSessions.has(sessionID)) return
|
|
|
- const syncing = syncingSessions.get(sessionID)
|
|
|
- if (syncing) return syncing
|
|
|
- const tracker = { messages: new Set<string>(), parts: new Set<string>() }
|
|
|
- hydratingSessions.set(sessionID, tracker)
|
|
|
- const task = (async () => {
|
|
|
- const [session, messages, todo, diff] = await Promise.all([
|
|
|
- sdk.client.session.get({ sessionID }, { throwOnError: true }),
|
|
|
- sdk.client.session.messages({ sessionID, limit: 100 }),
|
|
|
- sdk.client.session.todo({ sessionID }),
|
|
|
- sdk.client.session.diff({ sessionID }),
|
|
|
- ])
|
|
|
- setStore(
|
|
|
- produce((draft) => {
|
|
|
- const match = search(draft.session, sessionID, (s) => s.id)
|
|
|
- if (match.found) draft.session[match.index] = session.data!
|
|
|
- if (!match.found) draft.session.splice(match.index, 0, session.data!)
|
|
|
- draft.todo[sessionID] = todo.data ?? []
|
|
|
- const currentMessages = draft.message[sessionID] ?? []
|
|
|
- const infos = (messages.data ?? []).flatMap((message) => {
|
|
|
- if (!tracker.messages.has(message.info.id)) return [message.info]
|
|
|
- const current = currentMessages.find((item) => item.id === message.info.id)
|
|
|
- return current ? [current] : []
|
|
|
- })
|
|
|
- infos.push(
|
|
|
- ...currentMessages.filter(
|
|
|
- (message) => tracker.messages.has(message.id) && !infos.some((item) => item.id === message.id),
|
|
|
- ),
|
|
|
- )
|
|
|
- const removed = infos.slice(0, -100)
|
|
|
- const visible = infos.slice(-100)
|
|
|
- const visibleIDs = new Set(visible.map((message) => message.id))
|
|
|
- for (const message of messages.data ?? []) {
|
|
|
- if (!visibleIDs.has(message.info.id)) {
|
|
|
- delete draft.part[message.info.id]
|
|
|
- continue
|
|
|
- }
|
|
|
- const currentParts = draft.part[message.info.id] ?? []
|
|
|
- const parts = message.parts.flatMap((part) => {
|
|
|
- const current = currentParts.find((item) => item.id === part.id)
|
|
|
- if (tracker.parts.has(part.id)) return current ? [current] : []
|
|
|
- if (
|
|
|
- current &&
|
|
|
- (part.type === "text" || part.type === "reasoning") &&
|
|
|
- (current.type === "text" || current.type === "reasoning") &&
|
|
|
- part.text.length === 0 &&
|
|
|
- current.text.length > 0
|
|
|
- ) {
|
|
|
- return [current]
|
|
|
- }
|
|
|
- return [part]
|
|
|
- })
|
|
|
- parts.push(
|
|
|
- ...currentParts.filter(
|
|
|
- (part) => tracker.parts.has(part.id) && !parts.some((item) => item.id === part.id),
|
|
|
- ),
|
|
|
- )
|
|
|
- draft.part[message.info.id] = parts
|
|
|
- }
|
|
|
- for (const message of removed) delete draft.part[message.id]
|
|
|
- draft.message[sessionID] = visible
|
|
|
- draft.session_diff[sessionID] = diff.data ?? []
|
|
|
- }),
|
|
|
- )
|
|
|
- fullSyncedSessions.add(sessionID)
|
|
|
- })().finally(() => {
|
|
|
- syncingSessions.delete(sessionID)
|
|
|
- hydratingSessions.delete(sessionID)
|
|
|
- })
|
|
|
- syncingSessions.set(sessionID, task)
|
|
|
- return task
|
|
|
+ async refresh() {},
|
|
|
+ status(_sessionID: string) {
|
|
|
+ return "idle" as const
|
|
|
},
|
|
|
+ async sync(_sessionID: string) {},
|
|
|
},
|
|
|
- bootstrap,
|
|
|
+ async bootstrap(_input: { fatal?: boolean } = {}) {},
|
|
|
}
|
|
|
- return result
|
|
|
},
|
|
|
})
|