/** * What the panel needs from the outside world: the data it shows and the * side effects it triggers. `src/index.ts` wires both to src/data/* and * src/actions/*; the tests pass in-memory stubs, so the UI layer stays free * of I/O. * * 面板对外的两个端口:只读数据(DataSource)和副作用(ActionSource),由入口注入。 */ import type { Theme } from "@earendil-works/pi-coding-agent"; import type { ResolvedConfig } from "../config/config.ts"; import type { ConfigSaveResult, ConfigSnapshot, SettingsPatch } from "../config/config-store.ts"; /** Configuration I/O and runtime language application are injected by the entry point. */ export interface SettingsSource { read(): Promise; save(baseline: ConfigSnapshot, patch: SettingsPatch): Promise; apply(config: ResolvedConfig): void | Promise; } /** * pi's themes. The panel owns no theme vocabulary of its own: every color comes * from pi's active theme, and this port only lets the settings page list pi's * themes and switch to one — pi then persists the switch in its own * settings.json, so the page has no draft and no save step for it. * * `theme` is the live instance, so a switch made in pi's own /settings is picked * up on the next render too. `path` is undefined for themes pi knows without a * file (e.g. an in-memory instance). */ export interface ThemeSource { /** Every theme pi can load right now, re-scanned on each call. */ list(): readonly ThemeRef[]; /** The current theme, kept live so the panel follows pi's switches. */ readonly theme: Theme; /** Name of the theme pi has active — the same one it has saved. */ savedName(): string | undefined; /** Switch pi's active theme (persisted by pi). `error` is a plain message for the footer. */ set(name: string): { success: boolean; error?: string }; } export interface ThemeRef { name: string; /** Undefined for themes pi knows without a file (an in-memory instance). */ path: string | undefined; } import type { ContentBlock, ContextUsageInfo, DeleteMethod, EnterOutcome, ExportFormat, ExportTarget, ForkPoint, ListScope, RadiusLoginInteraction, RadiusLoginMethod, RestoreOptions, SessionFileChange, SessionFileState, SessionFileUpdate, SessionListFilter, SessionInfo, SessionRow, SessionShareDestination, SessionSortMode, ShareResult, TreeFilter, TreeRow, } from "../types.ts"; /** Async loaders injected by the entry point (they wrap src/data/*). */ export interface DataSource { /** Apply visibility before pinning / threaded sorting. */ listSessions(scope: ListScope, sort: SessionSortMode, pinned: readonly string[], filter: SessionListFilter): Promise; /** Reload shared metadata before listing so other pi windows' changes are seen. */ loadSessionState?(): Promise; loadTree(sessionFile: string, filter: TreeFilter): Promise; loadContent(sessionFile: string, leafEntryId?: string): Promise; /** `i` in SESSIONS: what /session shows; undefined when the file cannot be read. */ loadSessionInfo?(sessionFile: string): Promise; /** `u` in SESSIONS: context-window usage for the session; undefined when the file cannot be read. */ loadContextUsage?(sessionFile: string): Promise; /** `o` in SESSIONS: the user messages the fork selector lists (empty = nothing to fork). */ loadForkPoints?(sessionFile: string): Promise; /** `@`: pi's changelog as markdown (what /changelog shows). */ loadChangelog?(): Promise; } /** * Side effects injected by the entry point (they wrap src/actions/*). * 面板本身不做 I/O:复制、打标签、恢复会话、删除、改名都通过这里交给 actions 层。 */ export interface ActionSource { /** Copy the node's full text to the clipboard; `false` = the entry has no text. */ copyNodeText(sessionFile: string, entryId: string): Promise; /** Set, or clear with `undefined`, the label of a node. */ setNodeLabel(sessionFile: string, entryId: string, label: string | undefined): Promise; /** Enter in SESSIONS: make pi show this session (/resume). Rejects with the reason on failure. */ resumeSession(sessionFile: string): Promise; /** * Enter in TREE: continue the conversation from this node (/tree restore), * switching session first if needed; `options` is the summary choice. */ restoreNode(sessionFile: string, entryId: string, options: RestoreOptions): Promise; /** d in SESSIONS (after confirmation): remove the file; resolves to how it was removed. */ deleteSession?(sessionFile: string): Promise; /** Atomically merge a pin / archive / deletion change into the latest metadata. */ updateSessionState?(change: SessionFileChange): Promise; /** r in SESSIONS: set the display name ("" clears it). */ renameSession?(sessionFile: string, name: string): Promise; /** n in SESSIONS: start a fresh session, naming it when `name` is non-empty (/new). */ newSession?(name: string): Promise; /** o in SESSIONS (after picking a message and confirming): fork before that user message and open the fork (/fork). */ forkSession?(sessionFile: string, entryId: string): Promise; /** y in SESSIONS (after confirmation): clone the active branch to a new file (/clone). */ cloneSession?(sessionFile: string): Promise; /** c in SESSIONS: compact this conversation's active branch and open it (/compact). */ compactSession?(sessionFile: string, customInstructions?: string): Promise; /** Y in SESSIONS: copy the last assistant reply to the clipboard; `false` = no reply yet. */ copyLastReply?(sessionFile: string): Promise; /** y in the Session Info dialog: copy its text to the clipboard. */ copyText?(text: string): Promise; /** e in SESSIONS: where an export goes for what the user typed ("" = pi's default path); synchronous, no writing. */ exportTarget?(sessionFile: string, format: ExportFormat, input: string): ExportTarget; /** e in SESSIONS (once the path is picked, and confirmed when it exists): write the export, resolving to its path (/export). */ exportSession?(sessionFile: string, format: ExportFormat, outputPath: string): Promise; /** I in SESSIONS (after confirmation): copy a session JSONL into the session folder and switch to it (/import). */ importSession?(input: string): Promise; /** S in SESSIONS (after the destination menu): upload to Radius or a private GitHub gist (/share). */ shareSession?(sessionFile: string, destination: SessionShareDestination): Promise; /** Does Radius have a credential right now? `S` → Radius asks before uploading; false = sign in first. */ radiusAuthenticated?(): Promise; /** Sign in to Radius with pi's own `/login radius` flow; the interaction supplies the panel dialogs. */ loginRadius?(method: RadiusLoginMethod, interaction: RadiusLoginInteraction): Promise; /** S → Sign out of Radius (after confirmation): delete pi's stored Radius credential (what `/logout radius` does). */ logoutRadius?(): Promise; /** Open a URL in the platform browser (Radius sign-in). */ openUrl?(url: string): void; /** pi's `sessionShareDestination`, the `S` menu's initial cursor (absent = `auto`). */ shareDestination?: SessionShareDestination; }