Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,52 @@ Dependency security fixes and their regression commands are documented in
[`patches/README.md`](../patches/README.md). Source installs must use Bun so
the recorded `sprintf-js` patch is applied.

## Startup profiling

After `bun run sync:local`, run `bun run bench:startup 5` to measure the built
launcher and official runtime in a real PTY. The report separates first visible
output, the loading state, and the ready model/mode status. It uses a temporary
home with isolated credentials and disables catalog refresh and update checks;
it does not submit prompts or consume inference quota. The first sample also
includes runtime data initialization; later samples reuse that temporary home.
These are process-start measurements, not OS disk-cache cold-start measurements.

Interactive launches use a foreground TUI process and one runtime child process.
The lightweight `foreground.js` entry renders the real welcome frame, theme, and
composer before config bootstrap or runtime loading, so synchronous runtime
initialization cannot block typing or terminal rendering. A read-only appearance
lookup honors the saved regular/fullscreen mode and palette from the first frame;
it does not run settings bootstrap or migration. Syntax highlighting stays outside
this lightweight dependency graph. The ready UI adopts the existing terminal
instance rather than clearing, stopping, and restarting it. Startup input is a
draft: Enter never submits it until initialization completes, and the latest draft
is transferred at terminal handoff, including edits made while UI settings load.
Model access, slash commands, and resumed-session validation remain authoritative
in the runtime. Headless and app-server invocations keep their existing paths.

The child loads the same local TUI adapter, but its IPC entry exposes the existing
`TuiOptions` callbacks rather than owning the terminal. This requires no additional
patch to the official runtime and does not assume the public app-server API covers
all interactive features. Versioned, inherited Node IPC carries requests, streaming
events, permission callbacks, abort signals, and collections. It opens no network
listener, passes no account credentials over IPC, and starts no daemon.
Workflow reductions are serialized asynchronously; session IDs are refreshed with
runtime messages. The frontend owns terminal modes, local shells, login suspension,
and clipboard escape output; ordinary backend stdout/stderr goes to diagnostics.

Runtime death cancels pending calls and restores the terminal. Frontend shutdown
asks the child to finish and allows seven seconds for native cleanup before forced
termination (500 ms if the IPC host never became ready). A disconnected
child exits through the native TUI cleanup path. Source launches under Bun hand off
the foreground process to Node, avoiding incompatible Node/Bun IPC framing.

The build produces multiple chunks, all of which must be copied with the TUI `dist`
folder. Both `index.js` and `foreground.js` are required release entry points.
`test/tui/foreground.test.ts` verifies independent typing during a blocked runtime,
draft preservation without accidental submission, saved appearance, uninterrupted
regular/fullscreen terminal handoff, cancellation, and backend death;
`test/runtime-rpc.test.ts` covers callback, permission-abort, and transport behavior.

## pi-tui compatibility

The root package and TUI adapter use pi-tui 1.3.0. Theme detection uses
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"build": "tsdown",
"build:launcher": "tsdown --filter launcher",
"build:tui": "tsdown --filter tui",
"bench:startup": "bun scripts/bench-startup.ts",
"bench:tui-memory": "bun --expose-gc scripts/bench-tui-memory.ts",
"bench:tui-stream": "bun scripts/bench-tui-stream.ts",
"bench:tui-thinking": "bun scripts/bench-tui-thinking.ts",
Expand Down
9 changes: 9 additions & 0 deletions packages/zcode-tui/src/ai-sdk-warnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function suppressTuiAiSdkWarnings(): void {
const global = globalThis as typeof globalThis & {
AI_SDK_LOG_WARNINGS?: unknown;
};
if (typeof global.AI_SDK_LOG_WARNINGS === "function") return;
try {
global.AI_SDK_LOG_WARNINGS = false;
} catch {}
}
37 changes: 26 additions & 11 deletions packages/zcode-tui/src/dynamic-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class DynamicWorkflows {
private summaries: UnknownRecord[] = [];
private epoch = 0;
private loading?: Promise<void>;
private pending: Promise<unknown> = Promise.resolve();
private buffered: UnknownRecord[] = [];
private watermarks = new Map<string, number>();
private lastEvents = new Map<string, string>();
Expand All @@ -22,6 +23,7 @@ export class DynamicWorkflows {
this.state = undefined;
this.summaries = [];
this.loading = undefined;
this.pending = Promise.resolve();
this.buffered = [];
this.watermarks.clear();
this.lastEvents.clear();
Expand All @@ -41,20 +43,23 @@ export class DynamicWorkflows {
.map((run) => this.incomplete ? { ...run, progressIncomplete: true } : run);
}

accept(event: unknown): boolean {
if (!isRecord(event) || typeof event.runId !== "string" || typeof event.eventType !== "string") return false;
accept(event: unknown): Promise<boolean> {
if (!isRecord(event) || typeof event.runId !== "string" || typeof event.eventType !== "string") return Promise.resolve(false);
if (this.loading) {
if (this.buffered.length < maximumBufferedEvents) this.buffered.push(event);
else {
this.incomplete = true;
this.error = "Some workflow progress was omitted. Live runs cannot be fully replayed in this process; use /dwf list for runtime status.";
}
return false;
return Promise.resolve(false);
}
return this.apply(event);
const epoch = this.epoch;
const operation = this.pending.then(() => epoch === this.epoch ? this.apply(event, epoch) : false);
this.pending = operation;
return operation;
}

private apply(event: UnknownRecord): boolean {
private async apply(event: UnknownRecord, epoch: number): Promise<boolean> {
if (!this.adapter.reduceWorkflowRuns) return false;
const id = String(event.runId);
const sequence = typeof event.sequence === "number" ? event.sequence : undefined;
Expand All @@ -63,7 +68,8 @@ export class DynamicWorkflows {
if (sequence !== undefined && (sequence < watermark
|| sequence === watermark && this.lastEvents.get(id) === fingerprint)) return false;
try {
const state = this.adapter.reduceWorkflowRuns(this.state, event);
const state = await this.adapter.reduceWorkflowRuns(this.state, event);
if (epoch !== this.epoch) return false;
if (sequence !== undefined) this.watermarks.set(id, sequence);
this.lastEvents.set(id, fingerprint);
if (!state) return false;
Expand All @@ -78,6 +84,7 @@ export class DynamicWorkflows {
}
return true;
} catch (error) {
if (epoch !== this.epoch) return false;
this.error = `Workflow progress could not be read: ${error instanceof Error ? error.message : String(error)}`;
return false;
}
Expand All @@ -87,7 +94,7 @@ export class DynamicWorkflows {
if (this.loading) return this.loading;
const epoch = this.epoch;
if (!this.incomplete) this.error = undefined;
const operation = Promise.resolve().then(async () => {
const operation = this.pending.then(async () => {
// Read replay before summaries. Live events are buffered until replay is
// applied, then sequence watermarks discard duplicate deliveries.
const results = await Promise.allSettled([
Expand All @@ -97,21 +104,29 @@ export class DynamicWorkflows {
if (epoch !== this.epoch) return;
const [replay, summaries] = results;
if (replay.status === "fulfilled" && Array.isArray(replay.value)) {
for (const event of replay.value) if (isRecord(event)) this.apply(event);
for (const event of replay.value) {
if (epoch !== this.epoch) return;
if (isRecord(event)) await this.apply(event, epoch);
}
}
if (epoch !== this.epoch) return;
if (summaries.status === "fulfilled" && Array.isArray(summaries.value)) {
this.summaries = summaries.value.filter((value) => isRecord(value) && typeof value.runId === "string").slice(0, maximumRuns);
}
for (const result of results) if (result.status === "rejected") {
this.error = `Workflow history could not be loaded: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}. Run /workflows to retry.`;
}
}).finally(() => {
}).finally(async () => {
if (epoch !== this.epoch) return;
while (this.buffered.length && epoch === this.epoch) {
const event = this.buffered.shift()!;
await this.apply(event, epoch);
}
if (epoch !== this.epoch) return;
this.loading = undefined;
for (const event of this.buffered) this.apply(event);
this.buffered = [];
});
this.loading = operation;
this.pending = operation;
return operation;
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/zcode-tui/src/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { suppressTuiAiSdkWarnings } from "./ai-sdk-warnings.ts";
import { loadTuiStartupOptions } from "./startup.ts";
import type { TuiOptions } from "./types.ts";

suppressTuiAiSdkWarnings();

export async function runTui(options: TuiOptions): Promise<void> {
if (process.env.ZCODE_CLI_TUI_IPC === "1" && process.send) {
await (await import("./runtime-host.ts")).runRuntimeTuiHost(options);
return;
}
const loadStartupOptions = options.loadStartupOptions;
if (!loadStartupOptions) {
await (await import("./index.ts")).runTui(options);
return;
}

let implementation: typeof import("./index.ts") | undefined;
const readyOptions = await loadTuiStartupOptions({
...options,
loadStartupOptions: async () => {
const [startup, loaded] = await Promise.all([
loadStartupOptions(),
import("./index.ts")
]);
implementation = loaded;
return startup;
}
});
if (readyOptions && implementation) {
await implementation.runTui({ ...readyOptions, loadStartupOptions: undefined });
}
}
Loading
Loading