style: format web workspace
This commit is contained in:
parent
1c2cb01882
commit
54bb7209a4
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
SvelteKit static SPA for the Yoi workspace control plane.
|
||||
|
||||
The frontend is intentionally static. Workspace authority, validation, and API behavior live in the Rust `yoi-workspace-server` backend.
|
||||
The frontend is intentionally static. Workspace authority, validation, and API
|
||||
behavior live in the Rust `yoi-workspace-server` backend.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -22,7 +23,9 @@ cd web/workspace
|
|||
deno task dev
|
||||
```
|
||||
|
||||
The Vite dev server proxies `/api/*` to `http://127.0.0.1:8787`, so frontend hot reload works while the Rust backend serves the workspace API. Open the Vite URL printed by `deno task dev`.
|
||||
The Vite dev server proxies `/api/*` to `http://127.0.0.1:8787`, so frontend hot
|
||||
reload works while the Rust backend serves the workspace API. Open the Vite URL
|
||||
printed by `deno task dev`.
|
||||
|
||||
If you want to run the backend from the repository root instead:
|
||||
|
||||
|
|
@ -30,7 +33,10 @@ If you want to run the backend from the repository root instead:
|
|||
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
|
||||
```
|
||||
|
||||
The backend reads Workspace records from the Yoi server DB at `<data_dir>/server/server.db`. Run `cargo run -p yoi-workspace-server -- init --workspace .` first when the server DB has not been initialized.
|
||||
The backend reads Workspace records from the Yoi server DB at
|
||||
`<data_dir>/server/server.db`. Run
|
||||
`cargo run -p yoi-workspace-server -- init --workspace .` first when the server
|
||||
DB has not been initialized.
|
||||
|
||||
## Static build
|
||||
|
||||
|
|
@ -40,7 +46,8 @@ Build the SPA:
|
|||
deno task build
|
||||
```
|
||||
|
||||
Static asset packaging is a deployment concern. Local development normally uses the Vite dev server proxy plus the Rust backend command above.
|
||||
Static asset packaging is a deployment concern. Local development normally uses
|
||||
the Vite dev server proxy plus the Rust backend command above.
|
||||
|
||||
## Checks
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
|
|
|||
|
|
@ -12,112 +12,294 @@ export type WorkerStatus = "idle" | "running" | "paused";
|
|||
|
||||
export type TurnResult = "finished" | "paused";
|
||||
|
||||
export type InvokeKind = "user_send" | "notify" | "worker_event" | "system_reminder" | "wakeup";
|
||||
export type InvokeKind =
|
||||
| "user_send"
|
||||
| "notify"
|
||||
| "worker_event"
|
||||
| "system_reminder"
|
||||
| "wakeup";
|
||||
|
||||
export type RunResult = "finished" | "paused" | "limit_reached" | "rolled_back";
|
||||
|
||||
export type ErrorCode = "already_running" | "not_running" | "not_paused" | "provider_error" | "tool_error" | "invalid_request" | "internal";
|
||||
export type ErrorCode =
|
||||
| "already_running"
|
||||
| "not_running"
|
||||
| "not_paused"
|
||||
| "provider_error"
|
||||
| "tool_error"
|
||||
| "invalid_request"
|
||||
| "internal";
|
||||
|
||||
export type Permission = "read" | "write";
|
||||
|
||||
export type InFlightToolCallState = "pending" | "streaming_args" | "done";
|
||||
|
||||
export type ScopeRule = {
|
||||
/**
|
||||
/**
|
||||
* Target path. Must be absolute by the time a `Scope` is built from
|
||||
* this rule — relative paths are resolved per-layer against the
|
||||
* manifest file's directory (cwd for overlay layers) before cascade
|
||||
* merge.
|
||||
*/
|
||||
target: string,
|
||||
/**
|
||||
target: string;
|
||||
/**
|
||||
* Permission level this rule grants (allow) or caps strictly below
|
||||
* (deny).
|
||||
*/
|
||||
permission: Permission,
|
||||
/**
|
||||
permission: Permission;
|
||||
/**
|
||||
* When `false`, the rule only matches the target itself and its
|
||||
* direct children. Defaults to `true`.
|
||||
*/
|
||||
recursive: boolean, };
|
||||
recursive: boolean;
|
||||
};
|
||||
|
||||
export type CompletionEntry = { value: string, is_dir: boolean, };
|
||||
export type CompletionEntry = { value: string; is_dir: boolean };
|
||||
|
||||
export type RewindTargetId = { segment_id: string, user_input_entry_index: number, };
|
||||
export type RewindTargetId = {
|
||||
segment_id: string;
|
||||
user_input_entry_index: number;
|
||||
};
|
||||
|
||||
export type RewindTarget = { id: RewindTargetId, expected_head_entries: number, truncate_entries: number, turn_index: number, timestamp_ms: number | null, preview: string, eligible: boolean, disabled_reason: string | null, warning: string | null, };
|
||||
export type RewindTarget = {
|
||||
id: RewindTargetId;
|
||||
expected_head_entries: number;
|
||||
truncate_entries: number;
|
||||
turn_index: number;
|
||||
timestamp_ms: number | null;
|
||||
preview: string;
|
||||
eligible: boolean;
|
||||
disabled_reason: string | null;
|
||||
warning: string | null;
|
||||
};
|
||||
|
||||
export type RewindSummary = { truncated_to_entries: number, discarded_entries: number, tool_side_effect_warning: boolean, };
|
||||
export type RewindSummary = {
|
||||
truncated_to_entries: number;
|
||||
discarded_entries: number;
|
||||
tool_side_effect_warning: boolean;
|
||||
};
|
||||
|
||||
export type InFlightBlock = { "kind": "text", text: string, finished?: boolean, } | { "kind": "thinking", text: string, finished?: boolean, } | { "kind": "tool_call", id: string, name: string, args: string, state?: InFlightToolCallState, };
|
||||
export type InFlightBlock =
|
||||
| { "kind": "text"; text: string; finished?: boolean }
|
||||
| { "kind": "thinking"; text: string; finished?: boolean }
|
||||
| {
|
||||
"kind": "tool_call";
|
||||
id: string;
|
||||
name: string;
|
||||
args: string;
|
||||
state?: InFlightToolCallState;
|
||||
};
|
||||
|
||||
export type InFlightSnapshot = { blocks?: Array<InFlightBlock>, };
|
||||
export type InFlightSnapshot = { blocks?: Array<InFlightBlock> };
|
||||
|
||||
export type Greeting = { worker_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array<string>,
|
||||
/**
|
||||
export type Greeting = {
|
||||
worker_name: string;
|
||||
cwd: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
scope_summary: string;
|
||||
tools: Array<string>;
|
||||
/**
|
||||
* Model context window in tokens. Always filled by the Worker greeting.
|
||||
*/
|
||||
context_window: number,
|
||||
/**
|
||||
context_window: number;
|
||||
/**
|
||||
* Estimated current session context tokens at connect time.
|
||||
*/
|
||||
context_tokens: number, };
|
||||
context_tokens: number;
|
||||
};
|
||||
|
||||
export type Alert = { level: AlertLevel, source: AlertSource, message: string,
|
||||
/**
|
||||
export type Alert = {
|
||||
level: AlertLevel;
|
||||
source: AlertSource;
|
||||
message: string;
|
||||
/**
|
||||
* Milliseconds since the Unix epoch.
|
||||
*/
|
||||
timestamp_ms: number, };
|
||||
timestamp_ms: number;
|
||||
};
|
||||
|
||||
export type MemoryWorkerEvent = { worker: string, status: string, run_id: string, trigger: string, reason: string,
|
||||
/**
|
||||
export type MemoryWorkerEvent = {
|
||||
worker: string;
|
||||
status: string;
|
||||
run_id: string;
|
||||
trigger: string;
|
||||
reason: string;
|
||||
/**
|
||||
* Human-readable compact form for actionbar rendering.
|
||||
*/
|
||||
message: string,
|
||||
/**
|
||||
message: string;
|
||||
/**
|
||||
* Milliseconds since the Unix epoch.
|
||||
*/
|
||||
timestamp_ms: number, };
|
||||
timestamp_ms: number;
|
||||
};
|
||||
|
||||
export type Segment = { "kind": "text", content: string, } | { "kind": "paste", id: number, chars: number, lines: number, content: string, } | { "kind": "file_ref", path: string, } | { "kind": "unknown" };
|
||||
export type Segment =
|
||||
| { "kind": "text"; content: string }
|
||||
| {
|
||||
"kind": "paste";
|
||||
id: number;
|
||||
chars: number;
|
||||
lines: number;
|
||||
content: string;
|
||||
}
|
||||
| { "kind": "file_ref"; path: string }
|
||||
| { "kind": "unknown" };
|
||||
|
||||
export type WorkerEvent = { "kind": "turn_ended", worker_name: string, } | { "kind": "errored", worker_name: string, message: string, } | { "kind": "shut_down", worker_name: string, } | { "kind": "scope_sub_delegated",
|
||||
/**
|
||||
export type WorkerEvent =
|
||||
| { "kind": "turn_ended"; worker_name: string }
|
||||
| { "kind": "errored"; worker_name: string; message: string }
|
||||
| { "kind": "shut_down"; worker_name: string }
|
||||
| {
|
||||
"kind": "scope_sub_delegated";
|
||||
/**
|
||||
* Sub-delegating Worker (= the sender itself).
|
||||
*/
|
||||
parent_worker: string,
|
||||
/**
|
||||
parent_worker: string;
|
||||
/**
|
||||
* Name of the grandchild Worker.
|
||||
*/
|
||||
sub_worker: string,
|
||||
/**
|
||||
sub_worker: string;
|
||||
/**
|
||||
* Unix-socket path where the grandchild is reachable.
|
||||
*/
|
||||
sub_socket: string,
|
||||
/**
|
||||
sub_socket: string;
|
||||
/**
|
||||
* Scope delegated to the grandchild.
|
||||
*/
|
||||
scope: Array<ScopeRule>, };
|
||||
scope: Array<ScopeRule>;
|
||||
};
|
||||
|
||||
export type Method = { "method": "run", "params": { input: Array<Segment>, } } | { "method": "notify", "params": { message: string, auto_run?: boolean, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "resume" } | { "method": "cancel" } | { "method": "pause" } | { "method": "compact" } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown" } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } };
|
||||
export type Method =
|
||||
| { "method": "run"; "params": { input: Array<Segment> } }
|
||||
| { "method": "notify"; "params": { message: string; auto_run?: boolean } }
|
||||
| { "method": "worker_event"; "params": WorkerEvent }
|
||||
| { "method": "resume" }
|
||||
| { "method": "cancel" }
|
||||
| { "method": "pause" }
|
||||
| { "method": "compact" }
|
||||
| { "method": "list_rewind_targets" }
|
||||
| {
|
||||
"method": "rewind_to";
|
||||
"params": { target: RewindTargetId; expected_head_entries: number };
|
||||
}
|
||||
| { "method": "shutdown" }
|
||||
| {
|
||||
"method": "list_completions";
|
||||
"params": { kind: CompletionKind; prefix: string };
|
||||
}
|
||||
| { "method": "list_workers" }
|
||||
| { "method": "restore_worker"; "params": { name: string } }
|
||||
| { "method": "register_peer"; "params": { name: string } };
|
||||
|
||||
export type Event = { "event": "user_message", "data": { segments: Array<Segment>, } } | { "event": "system_item", "data": { item: unknown, } } | { "event": "invoke_start", "data": { kind: InvokeKind, } } | { "event": "turn_start", "data": { turn: number, } } | { "event": "turn_end", "data": { turn: number, result: TurnResult, } } | { "event": "llm_call_start", "data": { llm_call: number, } } | { "event": "llm_call_end", "data": { llm_call: number, } } | { "event": "llm_retry", "data": { llm_call: number,
|
||||
/**
|
||||
export type Event =
|
||||
| { "event": "user_message"; "data": { segments: Array<Segment> } }
|
||||
| { "event": "system_item"; "data": { item: unknown } }
|
||||
| { "event": "invoke_start"; "data": { kind: InvokeKind } }
|
||||
| { "event": "turn_start"; "data": { turn: number } }
|
||||
| { "event": "turn_end"; "data": { turn: number; result: TurnResult } }
|
||||
| { "event": "llm_call_start"; "data": { llm_call: number } }
|
||||
| { "event": "llm_call_end"; "data": { llm_call: number } }
|
||||
| {
|
||||
"event": "llm_retry";
|
||||
"data": {
|
||||
llm_call: number;
|
||||
/**
|
||||
* The attempt that just failed. 1 origin.
|
||||
*/
|
||||
failed_attempt: number, max_attempts: number, wait_ms: number, elapsed_ms: number, status?: number | null, error: string, } } | { "event": "llm_continuation", "data": { llm_call: number, attempt: number, max_attempts: number, reason: string, } } | { "event": "text_delta", "data": { text: string, } } | { "event": "text_done", "data": { text: string, } } | { "event": "thinking_start" } | { "event": "thinking_delta", "data": { text: string, } } | { "event": "thinking_done", "data": { text: string, } } | { "event": "tool_call_start", "data": { id: string, name: string, } } | { "event": "tool_call_args_delta", "data": { id: string, json: string, } } | { "event": "tool_call_done", "data": { id: string, name: string, arguments: string, } } | { "event": "tool_result", "data": { id: string,
|
||||
/**
|
||||
failed_attempt: number;
|
||||
max_attempts: number;
|
||||
wait_ms: number;
|
||||
elapsed_ms: number;
|
||||
status?: number | null;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
"event": "llm_continuation";
|
||||
"data": {
|
||||
llm_call: number;
|
||||
attempt: number;
|
||||
max_attempts: number;
|
||||
reason: string;
|
||||
};
|
||||
}
|
||||
| { "event": "text_delta"; "data": { text: string } }
|
||||
| { "event": "text_done"; "data": { text: string } }
|
||||
| { "event": "thinking_start" }
|
||||
| { "event": "thinking_delta"; "data": { text: string } }
|
||||
| { "event": "thinking_done"; "data": { text: string } }
|
||||
| { "event": "tool_call_start"; "data": { id: string; name: string } }
|
||||
| { "event": "tool_call_args_delta"; "data": { id: string; json: string } }
|
||||
| {
|
||||
"event": "tool_call_done";
|
||||
"data": { id: string; name: string; arguments: string };
|
||||
}
|
||||
| {
|
||||
"event": "tool_result";
|
||||
"data": {
|
||||
id: string;
|
||||
/**
|
||||
* Short human-readable summary. Always present; used by clients
|
||||
* that only want a 1-line rendering (e.g. collapsed views).
|
||||
*/
|
||||
summary: string,
|
||||
/**
|
||||
summary: string;
|
||||
/**
|
||||
* Full tool output. Absent when the tool chose to return
|
||||
* summary-only, or when the result was pruned.
|
||||
*/
|
||||
output?: string | null, is_error: boolean, } } | { "event": "usage", "data": { input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { entries: Array<unknown>, greeting: Greeting, status: WorkerStatus,
|
||||
/**
|
||||
output?: string | null;
|
||||
is_error: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
"event": "usage";
|
||||
"data": {
|
||||
input_tokens: number | null;
|
||||
output_tokens: number | null;
|
||||
cache_read_input_tokens?: number | null;
|
||||
};
|
||||
}
|
||||
| { "event": "run_end"; "data": { result: RunResult } }
|
||||
| { "event": "error"; "data": { code: ErrorCode; message: string } }
|
||||
| {
|
||||
"event": "snapshot";
|
||||
"data": {
|
||||
entries: Array<unknown>;
|
||||
greeting: Greeting;
|
||||
status: WorkerStatus;
|
||||
/**
|
||||
* Unfinished model output that has already streamed in the current
|
||||
* run but is not yet represented by committed snapshot entries.
|
||||
*/
|
||||
in_flight?: InFlightSnapshot, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" };
|
||||
in_flight?: InFlightSnapshot;
|
||||
};
|
||||
}
|
||||
| { "event": "segment_rotated"; "data": { entry: unknown } }
|
||||
| { "event": "status"; "data": { status: WorkerStatus } }
|
||||
| {
|
||||
"event": "completions";
|
||||
"data": { kind: CompletionKind; entries: Array<CompletionEntry> };
|
||||
}
|
||||
| {
|
||||
"event": "rewind_targets";
|
||||
"data": { head_entries: number; targets: Array<RewindTarget> };
|
||||
}
|
||||
| {
|
||||
"event": "rewind_applied";
|
||||
"data": {
|
||||
entries: Array<unknown>;
|
||||
input: Array<Segment>;
|
||||
summary: RewindSummary;
|
||||
};
|
||||
}
|
||||
| { "event": "workers_listed"; "data": { workers: unknown } }
|
||||
| { "event": "worker_restored"; "data": { result: unknown } }
|
||||
| { "event": "peer_registered"; "data": { result: unknown } }
|
||||
| { "event": "alert"; "data": Alert }
|
||||
| { "event": "memory_worker"; "data": MemoryWorkerEvent }
|
||||
| { "event": "compact_start" }
|
||||
| { "event": "compact_done"; "data": { new_segment_id: string } }
|
||||
| { "event": "compact_failed"; "data": { error: string } }
|
||||
| { "event": "shutdown" };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
export type WorkspaceAlertLevel = "success" | "info" | "warning" | "error" | "system" | "debug";
|
||||
export type WorkspaceAlertLevel =
|
||||
| "success"
|
||||
| "info"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "system"
|
||||
| "debug";
|
||||
|
||||
export type WorkspaceAlert = {
|
||||
id: string;
|
||||
|
|
@ -32,7 +38,8 @@ export function pushWorkspaceAlert(
|
|||
message: string,
|
||||
options: { title?: string; id?: string } = {},
|
||||
): string {
|
||||
const id = options.id ?? `${Date.now().toString(36)}-${(sequence++).toString(36)}`;
|
||||
const id = options.id ??
|
||||
`${Date.now().toString(36)}-${(sequence++).toString(36)}`;
|
||||
alerts = [
|
||||
...alerts.filter((alert) => alert.id !== id),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import {
|
||||
authenticationCredentialToJson,
|
||||
isPublicKeyCredential,
|
||||
prepareLoginOptions,
|
||||
prepareRegistrationOptions,
|
||||
registrationCredentialToJson,
|
||||
type DeviceApprovalResponse,
|
||||
isPublicKeyCredential,
|
||||
type PasskeyLoginOptionsResponse,
|
||||
type PasskeyRegistrationOptionsResponse,
|
||||
type PasskeyUserResponse,
|
||||
prepareLoginOptions,
|
||||
prepareRegistrationOptions,
|
||||
registrationCredentialToJson,
|
||||
type WhoamiResponse,
|
||||
} from "./model";
|
||||
|
||||
async function jsonOrThrow<T>(response: Response): Promise<T> {
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText}${text ? `: ${text}` : ""}`);
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? `: ${text}` : ""}`,
|
||||
);
|
||||
}
|
||||
return text ? JSON.parse(text) as T : (null as T);
|
||||
}
|
||||
|
|
@ -23,8 +25,12 @@ function browserOrigin(): string | null {
|
|||
return globalThis.location?.origin ?? null;
|
||||
}
|
||||
|
||||
export async function loadWhoami(fetcher: typeof fetch = fetch): Promise<WhoamiResponse> {
|
||||
return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow<WhoamiResponse>);
|
||||
export async function loadWhoami(
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<WhoamiResponse> {
|
||||
return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(
|
||||
jsonOrThrow<WhoamiResponse>,
|
||||
);
|
||||
}
|
||||
|
||||
export async function registerPasskey(
|
||||
|
|
@ -36,14 +42,20 @@ export async function registerPasskey(
|
|||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({ handle, display_name: displayName, browser_origin: browserOrigin() }),
|
||||
body: JSON.stringify({
|
||||
handle,
|
||||
display_name: displayName,
|
||||
browser_origin: browserOrigin(),
|
||||
}),
|
||||
}).then(jsonOrThrow<PasskeyRegistrationOptionsResponse>);
|
||||
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: prepareRegistrationOptions(options),
|
||||
});
|
||||
if (!isPublicKeyCredential(credential)) {
|
||||
throw new Error("Passkey registration did not return a public-key credential.");
|
||||
throw new Error(
|
||||
"Passkey registration did not return a public-key credential.",
|
||||
);
|
||||
}
|
||||
|
||||
return await fetcher("/api/auth/passkeys/registration/complete", {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ declare const Deno: {
|
|||
|
||||
function assertEquals<T>(actual: T, expected: T): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
throw new Error(
|
||||
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +21,9 @@ function bytes(buffer: BufferSource): number[] {
|
|||
if (buffer instanceof ArrayBuffer) {
|
||||
return [...new Uint8Array(buffer)];
|
||||
}
|
||||
return [...new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)];
|
||||
return [
|
||||
...new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength),
|
||||
];
|
||||
}
|
||||
|
||||
Deno.test("base64url helpers round-trip binary data", () => {
|
||||
|
|
@ -43,13 +47,20 @@ Deno.test("prepareRegistrationOptions decodes binary public key fields", () => {
|
|||
displayName: "Local User",
|
||||
},
|
||||
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
||||
excludeCredentials: [{ type: "public-key", id: "BwgJ" as unknown as BufferSource }],
|
||||
excludeCredentials: [{
|
||||
type: "public-key",
|
||||
id: "BwgJ" as unknown as BufferSource,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
assertEquals(bytes(options.challenge), [1, 2, 3]);
|
||||
assertEquals(bytes(options.user.id), [4, 5, 6]);
|
||||
assertEquals(bytes(options.excludeCredentials?.[0].id as BufferSource), [7, 8, 9]);
|
||||
assertEquals(bytes(options.excludeCredentials?.[0].id as BufferSource), [
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("prepareLoginOptions decodes challenge and allowed credential ids", () => {
|
||||
|
|
@ -57,10 +68,17 @@ Deno.test("prepareLoginOptions decodes challenge and allowed credential ids", ()
|
|||
challenge_id: "challenge-1",
|
||||
public_key: {
|
||||
challenge: "AQID" as unknown as BufferSource,
|
||||
allowCredentials: [{ type: "public-key", id: "BwgJ" as unknown as BufferSource }],
|
||||
allowCredentials: [{
|
||||
type: "public-key",
|
||||
id: "BwgJ" as unknown as BufferSource,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
assertEquals(bytes(options.challenge), [1, 2, 3]);
|
||||
assertEquals(bytes(options.allowCredentials?.[0].id as BufferSource), [7, 8, 9]);
|
||||
assertEquals(bytes(options.allowCredentials?.[0].id as BufferSource), [
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ export type WhoamiResponse = {
|
|||
|
||||
export type PasskeyRegistrationOptionsResponse = {
|
||||
challenge_id: string;
|
||||
public_key: PublicKeyCredentialCreationOptions | { publicKey: PublicKeyCredentialCreationOptions };
|
||||
public_key: PublicKeyCredentialCreationOptions | {
|
||||
publicKey: PublicKeyCredentialCreationOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export type PasskeyLoginOptionsResponse = {
|
||||
challenge_id: string;
|
||||
public_key: PublicKeyCredentialRequestOptions | { publicKey: PublicKeyCredentialRequestOptions };
|
||||
public_key: PublicKeyCredentialRequestOptions | {
|
||||
publicKey: PublicKeyCredentialRequestOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export type PasskeyUserResponse = {
|
||||
|
|
@ -65,7 +69,10 @@ export function bufferToBase64Url(buffer: ArrayBuffer): string {
|
|||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(
|
||||
/=+$/g,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapPublicKey<T>(value: T | { publicKey: T }): T {
|
||||
|
|
@ -79,12 +86,16 @@ export function prepareRegistrationOptions(
|
|||
options: PasskeyRegistrationOptionsResponse,
|
||||
): PublicKeyCredentialCreationOptions {
|
||||
const publicKey = structuredClone(unwrapPublicKey(options.public_key));
|
||||
publicKey.challenge = base64UrlToBuffer(publicKey.challenge as unknown as string);
|
||||
publicKey.challenge = base64UrlToBuffer(
|
||||
publicKey.challenge as unknown as string,
|
||||
);
|
||||
publicKey.user = {
|
||||
...publicKey.user,
|
||||
id: base64UrlToBuffer(publicKey.user.id as unknown as string),
|
||||
};
|
||||
publicKey.excludeCredentials = publicKey.excludeCredentials?.map((credential) => ({
|
||||
publicKey.excludeCredentials = publicKey.excludeCredentials?.map((
|
||||
credential,
|
||||
) => ({
|
||||
...credential,
|
||||
id: base64UrlToBuffer(credential.id as unknown as string),
|
||||
}));
|
||||
|
|
@ -95,8 +106,12 @@ export function prepareLoginOptions(
|
|||
options: PasskeyLoginOptionsResponse,
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
const publicKey = structuredClone(unwrapPublicKey(options.public_key));
|
||||
publicKey.challenge = base64UrlToBuffer(publicKey.challenge as unknown as string);
|
||||
publicKey.allowCredentials = publicKey.allowCredentials?.map((credential) => ({
|
||||
publicKey.challenge = base64UrlToBuffer(
|
||||
publicKey.challenge as unknown as string,
|
||||
);
|
||||
publicKey.allowCredentials = publicKey.allowCredentials?.map((
|
||||
credential,
|
||||
) => ({
|
||||
...credential,
|
||||
id: base64UrlToBuffer(credential.id as unknown as string),
|
||||
}));
|
||||
|
|
@ -131,11 +146,15 @@ export function authenticationCredentialToJson(
|
|||
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
|
||||
authenticatorData: bufferToBase64Url(response.authenticatorData),
|
||||
signature: bufferToBase64Url(response.signature),
|
||||
userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null,
|
||||
userHandle: response.userHandle
|
||||
? bufferToBase64Url(response.userHandle)
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function isPublicKeyCredential(credential: Credential | null): credential is PublicKeyCredential {
|
||||
export function isPublicKeyCredential(
|
||||
credential: Credential | null,
|
||||
): credential is PublicKeyCredential {
|
||||
return credential != null && credential.type === "public-key";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,16 @@ Deno.test("shouldSubmitChatKey still supports explicit Cmd+Enter behavior", () =
|
|||
});
|
||||
|
||||
Deno.test("shouldSubmitChatKey ignores IME composition and repeated Enter", () => {
|
||||
const options = { mode: "mod-enter" as const, modKey: "meta" as const, enabled: true };
|
||||
const options = {
|
||||
mode: "mod-enter" as const,
|
||||
modKey: "meta" as const,
|
||||
enabled: true,
|
||||
};
|
||||
assertEquals(
|
||||
shouldSubmitChatKey({ key: "Enter", metaKey: true, isComposing: true }, options),
|
||||
shouldSubmitChatKey(
|
||||
{ key: "Enter", metaKey: true, isComposing: true },
|
||||
options,
|
||||
),
|
||||
false,
|
||||
);
|
||||
assertEquals(
|
||||
|
|
@ -69,7 +76,14 @@ Deno.test("shouldSubmitChatKey ignores IME composition and repeated Enter", () =
|
|||
});
|
||||
|
||||
Deno.test("shouldSubmitChatKey supports enter submit mode", () => {
|
||||
const options = { mode: "enter" as const, modKey: "meta" as const, enabled: true };
|
||||
const options = {
|
||||
mode: "enter" as const,
|
||||
modKey: "meta" as const,
|
||||
enabled: true,
|
||||
};
|
||||
assert(shouldSubmitChatKey({ key: "Enter" }, options), "Enter should submit");
|
||||
assertEquals(shouldSubmitChatKey({ key: "Enter", shiftKey: true }, options), false);
|
||||
assertEquals(
|
||||
shouldSubmitChatKey({ key: "Enter", shiftKey: true }, options),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ export type ChatSubmitKeyEventLike = {
|
|||
which?: number;
|
||||
};
|
||||
|
||||
function normalizeOptions(options: ChatSubmitOptions): NormalizedChatSubmitOptions {
|
||||
function normalizeOptions(
|
||||
options: ChatSubmitOptions,
|
||||
): NormalizedChatSubmitOptions {
|
||||
return {
|
||||
mode: "mod-enter",
|
||||
modKey: "auto",
|
||||
|
|
@ -47,7 +49,8 @@ function isApplePlatform(): boolean {
|
|||
}
|
||||
const platform = navigator.platform || "";
|
||||
const userAgent = navigator.userAgent || "";
|
||||
return /Mac|iPhone|iPad|iPod/.test(platform) || /Mac|iPhone|iPad|iPod/.test(userAgent);
|
||||
return /Mac|iPhone|iPad|iPod/.test(platform) ||
|
||||
/Mac|iPhone|iPad|iPod/.test(userAgent);
|
||||
}
|
||||
|
||||
function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" {
|
||||
|
|
@ -57,8 +60,13 @@ function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" {
|
|||
return modKey;
|
||||
}
|
||||
|
||||
function isModPressed(event: ChatSubmitKeyEventLike, modKey: ChatSubmitModKey): boolean {
|
||||
return resolveModKey(modKey) === "meta" ? event.metaKey === true : event.ctrlKey === true;
|
||||
function isModPressed(
|
||||
event: ChatSubmitKeyEventLike,
|
||||
modKey: ChatSubmitModKey,
|
||||
): boolean {
|
||||
return resolveModKey(modKey) === "meta"
|
||||
? event.metaKey === true
|
||||
: event.ctrlKey === true;
|
||||
}
|
||||
|
||||
export function shouldSubmitChatKey(
|
||||
|
|
@ -79,7 +87,10 @@ export function shouldSubmitChatKey(
|
|||
return isModPressed(event, options.modKey);
|
||||
}
|
||||
|
||||
export function chatSubmit(node: HTMLTextAreaElement, options: ChatSubmitOptions) {
|
||||
export function chatSubmit(
|
||||
node: HTMLTextAreaElement,
|
||||
options: ChatSubmitOptions,
|
||||
) {
|
||||
let current = normalizeOptions(options);
|
||||
let isComposing = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ Deno.test("parseSigilSegments turns file sigils into file refs", () => {
|
|||
});
|
||||
|
||||
Deno.test("parseSigilSegments leaves hash sigils as plain text", () => {
|
||||
assertEquals(parseSigilSegments("ask #memory"), [{ kind: "text", content: "ask #memory" }]);
|
||||
assertEquals(parseSigilSegments("ask #memory"), [{
|
||||
kind: "text",
|
||||
content: "ask #memory",
|
||||
}]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ type CommandSpec = {
|
|||
const COMMANDS: Record<string, CommandSpec> = {
|
||||
help: {
|
||||
usage: ":help [command]",
|
||||
description: "Show available Web Console commands or details for one command.",
|
||||
description:
|
||||
"Show available Web Console commands or details for one command.",
|
||||
},
|
||||
"?": {
|
||||
usage: ":? [command]",
|
||||
|
|
@ -49,7 +50,8 @@ const COMMANDS: Record<string, CommandSpec> = {
|
|||
},
|
||||
peer: {
|
||||
usage: ":peer <worker-name>",
|
||||
description: "Register another existing Worker as a reciprocal metadata peer.",
|
||||
description:
|
||||
"Register another existing Worker as a reciprocal metadata peer.",
|
||||
},
|
||||
system: {
|
||||
usage: ":system <message>",
|
||||
|
|
@ -71,7 +73,9 @@ export function buildComposerRequest(value: string): ComposerCommandResult {
|
|||
request: {
|
||||
kind: "user",
|
||||
content,
|
||||
segments: segments.some((segment) => segment.kind !== "text") ? segments : undefined,
|
||||
segments: segments.some((segment) => segment.kind !== "text")
|
||||
? segments
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -97,13 +101,21 @@ function buildColonCommand(commandLine: string): ComposerCommandResult {
|
|||
if (argv.length > 0) {
|
||||
return invalidUsage("compact");
|
||||
}
|
||||
return { ok: true, request: { kind: "compact", content: "" }, notice: "compact requested" };
|
||||
return {
|
||||
ok: true,
|
||||
request: { kind: "compact", content: "" },
|
||||
notice: "compact requested",
|
||||
};
|
||||
case "rewind":
|
||||
case "rollback":
|
||||
if (argv.length > 0) {
|
||||
return invalidUsage("rewind");
|
||||
}
|
||||
return { ok: true, request: { kind: "list_rewind_targets", content: "" }, notice: "rewind targets requested" };
|
||||
return {
|
||||
ok: true,
|
||||
request: { kind: "list_rewind_targets", content: "" },
|
||||
notice: "rewind targets requested",
|
||||
};
|
||||
case "peer":
|
||||
if (argv.length !== 1) {
|
||||
return invalidUsage("peer");
|
||||
|
|
@ -156,7 +168,10 @@ function helpCommand(argv: string[]): ComposerCommandResult {
|
|||
}
|
||||
|
||||
function invalidUsage(name: string): ComposerCommandResult {
|
||||
return { ok: false, message: `Invalid arguments. Usage: ${COMMANDS[name].usage}` };
|
||||
return {
|
||||
ok: false,
|
||||
message: `Invalid arguments. Usage: ${COMMANDS[name].usage}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSigilSegments(input: string): Segment[] {
|
||||
|
|
@ -178,7 +193,9 @@ export function parseSigilSegments(input: string): Segment[] {
|
|||
if (cursor < input.length) {
|
||||
segments.push({ kind: "text", content: input.slice(cursor) });
|
||||
}
|
||||
return coalesceTextSegments(segments.length > 0 ? segments : [{ kind: "text", content: input }]);
|
||||
return coalesceTextSegments(
|
||||
segments.length > 0 ? segments : [{ kind: "text", content: input }],
|
||||
);
|
||||
}
|
||||
|
||||
function sigilSegment(sigil: string, value: string): Segment {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { Event } from "$lib/generated/protocol";
|
||||
import {
|
||||
type ConsoleLine,
|
||||
createConsoleProjector,
|
||||
projectConsole,
|
||||
segmentsToText,
|
||||
selectConsoleTimelineLines,
|
||||
workerConsoleHref,
|
||||
type ConsoleLine,
|
||||
} from "./model.ts";
|
||||
|
||||
declare const Deno: {
|
||||
|
|
@ -68,8 +68,6 @@ Deno.test("workerConsoleHref encodes runtime and worker target authority", () =>
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Deno.test("projectConsole projects visible protocol rows", () => {
|
||||
const projection = projectConsole([
|
||||
{
|
||||
|
|
@ -192,7 +190,8 @@ Deno.test("projectConsole groups tool call lifecycle into one Call block", () =>
|
|||
data: {
|
||||
id: "call-1",
|
||||
summary: "command completed",
|
||||
output: "/repo\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12",
|
||||
output:
|
||||
"/repo\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
|
|
@ -269,7 +268,10 @@ Deno.test("projectConsole caps default tool request and result previews", () =>
|
|||
assertEquals(line.title, "Call · CustomTool");
|
||||
assertEquals(line.body.split("\n").length, 7);
|
||||
assert(line.body.includes("CustomTool — done"), "tool state should be shown");
|
||||
assert(line.body.includes('"first": "one"'), "request preview should be shown");
|
||||
assert(
|
||||
line.body.includes('"first": "one"'),
|
||||
"request preview should be shown",
|
||||
);
|
||||
assert(line.body.includes("out1"), "result preview should be shown");
|
||||
assert(!line.body.includes("third"), "request preview should be capped");
|
||||
assert(!line.body.includes("out3"), "result preview should be capped");
|
||||
|
|
@ -305,7 +307,10 @@ Deno.test("projectConsole shows Grep query and caps result preview to five entri
|
|||
|
||||
const [line] = projection.lines.filter((line) => line.kind === "tool");
|
||||
assertEquals(line.title, "Call · Grep");
|
||||
assert(line.body.includes("Grep — 6 matches"), "Grep summary should be shown");
|
||||
assert(
|
||||
line.body.includes("Grep — 6 matches"),
|
||||
"Grep summary should be shown",
|
||||
);
|
||||
assert(line.body.includes("query: needle"), "Grep query should be shown");
|
||||
assert(line.body.includes("hit1"), "first result should be shown");
|
||||
assert(line.body.includes("hit5"), "fifth result should be shown");
|
||||
|
|
@ -840,7 +845,9 @@ Deno.test("projectConsole renders snapshot entries and in-flight output", () =>
|
|||
|
||||
assertEquals(projection.status, "running");
|
||||
assertEquals(
|
||||
projection.lines.map((line) => `${line.kind}:${line.body}:${line.streaming}`),
|
||||
projection.lines.map((line) =>
|
||||
`${line.kind}:${line.body}:${line.streaming}`
|
||||
),
|
||||
[
|
||||
"user:seed user:false",
|
||||
"user:new user:false",
|
||||
|
|
@ -903,7 +910,9 @@ Deno.test("selectConsoleTimelineLines keeps all users and only last assistant pe
|
|||
];
|
||||
|
||||
assertEquals(
|
||||
selectConsoleTimelineLines(items).map(({ item, index }) => `${index}:${item.id}`),
|
||||
selectConsoleTimelineLines(items).map(({ item, index }) =>
|
||||
`${index}:${item.id}`
|
||||
),
|
||||
["0:u1", "3:a2", "4:u2", "6:a4"],
|
||||
);
|
||||
});
|
||||
|
|
@ -940,7 +949,10 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
|||
data: {
|
||||
id: "write-rel",
|
||||
name: "Write",
|
||||
arguments: JSON.stringify({ file_path: "/repo/out.txt", content: "ok" }),
|
||||
arguments: JSON.stringify({
|
||||
file_path: "/repo/out.txt",
|
||||
content: "ok",
|
||||
}),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
|
|
@ -1001,7 +1013,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
|||
data: {
|
||||
id: "glob-rel",
|
||||
summary: "Found 2 file(s) matching **/*.rs",
|
||||
output: "Found 2 file(s) matching **/*.rs\n/repo/src/main.rs\n/outside/lib.rs",
|
||||
output:
|
||||
"Found 2 file(s) matching **/*.rs\n/repo/src/main.rs\n/outside/lib.rs",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
|
|
@ -1031,14 +1044,18 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
|||
},
|
||||
]);
|
||||
|
||||
const bodies = projection.lines.filter((line) => line.kind === "tool").map((line) => line.body);
|
||||
const bodies = projection.lines.filter((line) => line.kind === "tool").map((
|
||||
line,
|
||||
) => line.body);
|
||||
assertEquals(bodies[0], "Read — 1 file read\n src/main.rs");
|
||||
assert(
|
||||
projection.lines[0].detail?.includes("from src/main.rs"),
|
||||
"Read summary detail path should be relative",
|
||||
);
|
||||
assert(
|
||||
bodies.some((body) => body.includes("Write — out.txt") && body.includes("Wrote out.txt")),
|
||||
bodies.some((body) =>
|
||||
body.includes("Write — out.txt") && body.includes("Wrote out.txt")
|
||||
),
|
||||
"Write header and known result path should be relative",
|
||||
);
|
||||
assert(
|
||||
|
|
@ -1055,7 +1072,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
|||
);
|
||||
assert(
|
||||
bodies.some((body) =>
|
||||
body.includes("src/main.rs:12:needle") && body.includes("/outside/lib.rs:1:needle")
|
||||
body.includes("src/main.rs:12:needle") &&
|
||||
body.includes("/outside/lib.rs:1:needle")
|
||||
),
|
||||
"Grep should relativize line-start cwd paths and keep outside paths absolute",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type {
|
||||
Event as ProtocolEvent,
|
||||
Alert,
|
||||
Event as ProtocolEvent,
|
||||
InFlightBlock,
|
||||
InFlightToolCallState,
|
||||
Segment,
|
||||
|
|
@ -148,7 +148,10 @@ export function emptyConsoleProjection(): ConsoleProjection {
|
|||
export function projectConsole(
|
||||
events: ConsoleEventInput[] = [],
|
||||
): ConsoleProjection {
|
||||
const projection = events.reduce(applyProtocolEvent, emptyConsoleProjection());
|
||||
const projection = events.reduce(
|
||||
applyProtocolEvent,
|
||||
emptyConsoleProjection(),
|
||||
);
|
||||
return projectVisibleConsole(projection);
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +174,9 @@ export function createConsoleProjector() {
|
|||
};
|
||||
}
|
||||
|
||||
function projectVisibleConsole(projection: ConsoleProjection): ConsoleProjection {
|
||||
function projectVisibleConsole(
|
||||
projection: ConsoleProjection,
|
||||
): ConsoleProjection {
|
||||
return {
|
||||
...projection,
|
||||
lines: aggregateReadToolLines(projection.lines),
|
||||
|
|
@ -302,7 +307,9 @@ export function applyProtocolEvent(
|
|||
next.status = event.data.status;
|
||||
break;
|
||||
case "segment_rotated":
|
||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [event.data.entry], next.cwd);
|
||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [
|
||||
event.data.entry,
|
||||
], next.cwd);
|
||||
break;
|
||||
case "invoke_start":
|
||||
case "turn_start":
|
||||
|
|
@ -730,7 +737,9 @@ function readDetail(toolCall: ToolCallView): string {
|
|||
`state: ${stateSuffix(toolCall.state)}`,
|
||||
`path: ${readPath(toolCall)}`,
|
||||
toolCall.summary
|
||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
||||
? `summary: ${
|
||||
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)
|
||||
}`
|
||||
: undefined,
|
||||
]);
|
||||
}
|
||||
|
|
@ -902,7 +911,9 @@ function toolCallDetail(toolCall: ToolCallView): string {
|
|||
`id: ${toolCall.id}`,
|
||||
`state: ${stateSuffix(toolCall.state)}`,
|
||||
toolCall.summary
|
||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
||||
? `summary: ${
|
||||
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)
|
||||
}`
|
||||
: undefined,
|
||||
argsText(toolCall) ? `arguments:\n${argsText(toolCall)}` : undefined,
|
||||
]);
|
||||
|
|
@ -1155,7 +1166,8 @@ function applyExtensionEntry(
|
|||
}
|
||||
const blockId = stringField(payload, "block_id") || "compact";
|
||||
const state = stringField(payload, "state") || "running";
|
||||
const message = stringField(payload, "message") || compactMessageForState(state, payload);
|
||||
const message = stringField(payload, "message") ||
|
||||
compactMessageForState(state, payload);
|
||||
upsertStatusLine(
|
||||
projection,
|
||||
blockId,
|
||||
|
|
@ -1204,26 +1216,38 @@ function applyLoggedItem(
|
|||
break;
|
||||
}
|
||||
case "reasoning": {
|
||||
const text = stringField(item, "text") ?? arrayField(item, "summary").filter((value) => typeof value === "string").join("\n");
|
||||
const text = stringField(item, "text") ??
|
||||
arrayField(item, "summary").filter((value) => typeof value === "string")
|
||||
.join("\n");
|
||||
if (text) {
|
||||
projection.lines.push(line(eventId, "thinking", "Thought", text));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tool_call":
|
||||
upsertToolCall(projection, eventId, stringField(item, "call_id") ?? eventId, {
|
||||
upsertToolCall(
|
||||
projection,
|
||||
eventId,
|
||||
stringField(item, "call_id") ?? eventId,
|
||||
{
|
||||
name: stringField(item, "name") ?? "Tool",
|
||||
arguments: stringField(item, "arguments") ?? "",
|
||||
argsStream: stringField(item, "arguments") ?? "",
|
||||
state: "running",
|
||||
});
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "tool_result":
|
||||
attachToolResult(projection, eventId, stringField(item, "call_id") ?? eventId, {
|
||||
attachToolResult(
|
||||
projection,
|
||||
eventId,
|
||||
stringField(item, "call_id") ?? eventId,
|
||||
{
|
||||
summary: stringField(item, "summary") ?? "",
|
||||
output: stringField(item, "content"),
|
||||
isError: item["is_error"] === true,
|
||||
});
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ function assert(condition: unknown, message: string): asserts condition {
|
|||
}
|
||||
|
||||
Deno.test("workspace app css uses bundled UI fonts", async () => {
|
||||
const appCss = await Deno.readTextFile(new URL("./../../../app.css", import.meta.url));
|
||||
const appCss = await Deno.readTextFile(
|
||||
new URL("./../../../app.css", import.meta.url),
|
||||
);
|
||||
assert(
|
||||
appCss.includes("gen-interface-jp/400.css") &&
|
||||
appCss.includes("gen-interface-jp/500.css") &&
|
||||
|
|
@ -34,12 +36,20 @@ Deno.test("workspace app css uses bundled UI fonts", async () => {
|
|||
});
|
||||
|
||||
Deno.test("workspace feature css is owned outside app css", async () => {
|
||||
const appCss = await Deno.readTextFile(new URL("./../../../app.css", import.meta.url));
|
||||
const appCss = await Deno.readTextFile(
|
||||
new URL("./../../../app.css", import.meta.url),
|
||||
);
|
||||
const workspaceLayout = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/+layout.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const settingsLayout = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/settings/+layout.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/settings/+layout.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const accountPage = await Deno.readTextFile(
|
||||
new URL("./../../../routes/account/+page.svelte", import.meta.url),
|
||||
|
|
@ -86,7 +96,9 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
|||
workspacePage.includes("runtimeSettingsHref") &&
|
||||
workspacePage.includes("workersHref") &&
|
||||
workspacePage.includes("workspaceRoute(workspaceId, '/tickets')") &&
|
||||
workspacePage.includes("workspaceRoute(workspaceId, '/settings/runtimes')") &&
|
||||
workspacePage.includes(
|
||||
"workspaceRoute(workspaceId, '/settings/runtimes')",
|
||||
) &&
|
||||
workspacePage.includes("workspaceRoute(workspaceId, '/workers')"),
|
||||
"top workspace page should link to Tickets, Runtime Inventory under Settings, and the Workers page",
|
||||
);
|
||||
|
|
@ -99,7 +111,7 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
|||
workersPage.includes("workerConsoleHref(worker, data.workspaceId)") &&
|
||||
workersPage.includes('<table class="workers-table">') &&
|
||||
workersPage.includes('class="icon-action"') &&
|
||||
workersPage.includes('Delete ${worker.label}'),
|
||||
workersPage.includes("Delete ${worker.label}"),
|
||||
"dedicated Workers page should expose a table, console link target, and icon actions per Worker",
|
||||
);
|
||||
assert(
|
||||
|
|
@ -122,10 +134,16 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async
|
|||
new URL("../sidebar/TicketsNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const ticketsLoad = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/tickets/+page.ts", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/tickets/+page.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const ticketsPage = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/tickets/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/tickets/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const ticketDetailLoad = await Deno.readTextFile(
|
||||
new URL(
|
||||
|
|
@ -146,14 +164,18 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async
|
|||
"Tickets sidebar section should link to the workspace Tickets surface",
|
||||
);
|
||||
assert(
|
||||
ticketsLoad.includes('`${workspaceApiPath(params.workspaceId, "/tickets")}?limit=1000`') &&
|
||||
ticketsLoad.includes(
|
||||
'`${workspaceApiPath(params.workspaceId, "/tickets")}?limit=1000`',
|
||||
) &&
|
||||
ticketsPage.includes("Notion-style filtering and sorting") &&
|
||||
ticketsPage.includes("toggleSort('updated_at')") &&
|
||||
ticketsPage.includes("bind:value={visibilityFilter}") &&
|
||||
ticketsPage.includes("sortKey = $state<SortKey>('panel')") &&
|
||||
ticketsPage.includes("workspace_action_priority") &&
|
||||
ticketsPage.includes("bind:value={stateFilter}") &&
|
||||
ticketsPage.includes("workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)"),
|
||||
ticketsPage.includes(
|
||||
"workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)",
|
||||
),
|
||||
"Tickets list should read the workspace-scoped Ticket API and expose sortable/filterable table links",
|
||||
);
|
||||
assert(
|
||||
|
|
@ -170,10 +192,16 @@ Deno.test("workspace Memory Staging surface uses read-only scoped memory API", a
|
|||
new URL("../sidebar/MemoryNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const memoryLoad = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.ts", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/memory/staging/+page.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const memoryPage = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/memory/staging/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
|
|
@ -182,7 +210,8 @@ Deno.test("workspace Memory Staging surface uses read-only scoped memory API", a
|
|||
"Memory sidebar section should link to the workspace Memory Staging surface",
|
||||
);
|
||||
assert(
|
||||
memoryLoad.includes("workspaceApiPath(params.workspaceId, '/memory/staging')") &&
|
||||
memoryLoad.includes("workspaceApiPath(params.workspaceId") &&
|
||||
memoryLoad.includes('"/memory/staging"') &&
|
||||
memoryPage.includes("Memory Staging") &&
|
||||
memoryPage.includes("Workspace Server memory authority") &&
|
||||
memoryPage.includes("data.staging.data.invalid_count") &&
|
||||
|
|
@ -236,7 +265,9 @@ Deno.test("Worker Console renders markdown only for message rows", async () => {
|
|||
assert(
|
||||
consoleLine.includes("function shouldRenderMarkdown") &&
|
||||
consoleLine.includes("item.kind === 'tool'") &&
|
||||
consoleLine.includes('<p class="console-plain-text">{bodyTextAfterToolSummary(item)}</p>') &&
|
||||
consoleLine.includes(
|
||||
'<p class="console-plain-text">{bodyTextAfterToolSummary(item)}</p>',
|
||||
) &&
|
||||
consoleLine.includes("{:else if shouldRenderMarkdown(item)}") &&
|
||||
consoleLine.includes("<RichMarkdown text={item.body || '—'} />"),
|
||||
"Console should keep markdown rendering to user/assistant/system message bodies and render tool text literally",
|
||||
|
|
@ -249,7 +280,9 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
|
|||
);
|
||||
|
||||
assert(
|
||||
consoleLine.includes('<div class="console-diff" role="group" aria-label="Edit diff">') &&
|
||||
consoleLine.includes(
|
||||
'<div class="console-diff" role="group" aria-label="Edit diff">',
|
||||
) &&
|
||||
consoleLine.includes("{#each item.diff as diffLine}") &&
|
||||
consoleLine.includes("class={`diff-line ${diffLine.kind}`}") &&
|
||||
!consoleLine.includes('<pre class="console-diff"'),
|
||||
|
|
@ -276,7 +309,7 @@ Deno.test("Worker Console exposes a foldable timeline beside the scroll body", a
|
|||
consolePage.includes("jumpToTimelineMark") &&
|
||||
consoleLine.includes("data-console-line-id={item.id}") &&
|
||||
consolePage.includes("class:timeline-open={timelineOpen}") &&
|
||||
consolePage.includes("class=\"timeline-fold\"") &&
|
||||
consolePage.includes('class="timeline-fold"') &&
|
||||
consolePage.includes("expanded={timelineOpen}") &&
|
||||
!consolePage.includes("{#if timelineOpen}") &&
|
||||
consolePage.includes("handleTimelineRailPointerDown") &&
|
||||
|
|
@ -301,17 +334,19 @@ Deno.test("Worker Console composer fits to content without manual resize", async
|
|||
);
|
||||
assert(
|
||||
consolePage.includes("use:fitTextarea={{ value: draft, maxRows: 10 }}") &&
|
||||
consolePage.includes("<div class=\"composer-input-shell\">") &&
|
||||
consolePage.includes('<div class="composer-input-shell">') &&
|
||||
!consolePage.includes("handleComposerShellClick") &&
|
||||
consolePage.includes("bind:this={composerTextareaElement}") &&
|
||||
consolePage.includes('event.key === "PageUp" || event.key === "PageDown"') &&
|
||||
consolePage.includes(
|
||||
'event.key === "PageUp" || event.key === "PageDown"',
|
||||
) &&
|
||||
consolePage.includes("scrollConsoleByPage") &&
|
||||
consolePage.includes("class=\"composer-input-footer\"") &&
|
||||
consolePage.includes('class="composer-input-footer"') &&
|
||||
consolePage.includes("pointer-events: none") &&
|
||||
consolePage.includes("class=\"composer-footer-slot\"") &&
|
||||
consolePage.includes("class=\"composer-send-button\"") &&
|
||||
consolePage.includes('class="composer-footer-slot"') &&
|
||||
consolePage.includes('class="composer-send-button"') &&
|
||||
consolePage.includes("pointer-events: auto") &&
|
||||
consolePage.includes("class=\"composer-send-icon\"") &&
|
||||
consolePage.includes('class="composer-send-icon"') &&
|
||||
consolePage.includes('d="M8 6L12 2L16 6"') &&
|
||||
consolePage.includes(".console-composer textarea") &&
|
||||
consolePage.includes("resize: none") &&
|
||||
|
|
@ -366,14 +401,16 @@ Deno.test("workspace Runtime inventory lives under Settings admin routes", async
|
|||
assert(
|
||||
!sidebar.includes("RuntimesNavSection") &&
|
||||
settingsModel.includes('id: "runtime-inventory"') &&
|
||||
settingsModel.includes('return `${SETTINGS_ROUTE}/runtimes`;'),
|
||||
settingsModel.includes("return `${SETTINGS_ROUTE}/runtimes`;"),
|
||||
"Runtime inventory should be admin Settings navigation, not primary workspace sidebar navigation",
|
||||
);
|
||||
assert(
|
||||
runtimesPage.includes("Runtime Inventory") &&
|
||||
runtimesPage.includes("Open workdirs") &&
|
||||
runtimesPage.includes("runtimes-table") &&
|
||||
runtimesPage.includes("/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs"),
|
||||
runtimesPage.includes(
|
||||
"/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs",
|
||||
),
|
||||
"Settings Runtime Inventory page should table Runtimes and link to each Runtime's workdirs",
|
||||
);
|
||||
assert(
|
||||
|
|
@ -431,7 +468,9 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac
|
|||
assert(
|
||||
consolePage.includes("workspaceApiPath(workspaceId, path)") &&
|
||||
consolePage.includes("workerApiPath(") &&
|
||||
consolePage.includes("`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(") &&
|
||||
consolePage.includes(
|
||||
"`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(",
|
||||
) &&
|
||||
consolePage.includes("target.workerId"),
|
||||
"Worker detail should use the scoped backend Worker detail API",
|
||||
);
|
||||
|
|
@ -460,14 +499,16 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac
|
|||
"target-change effect should load data without depending on manual refresh state reads",
|
||||
);
|
||||
assert(
|
||||
consolePage.includes('const workerRunning = $derived(workerState === "running");') &&
|
||||
consolePage.includes(
|
||||
'const workerRunning = $derived(workerState === "running");',
|
||||
) &&
|
||||
consolePage.includes(
|
||||
'const composerEditable = $derived(protocolState === "open" && !sending);',
|
||||
) &&
|
||||
consolePage.includes('sendControl({ method: "cancel" }, "Stop")') &&
|
||||
consolePage.includes("enabled: canSubmitDraft") &&
|
||||
consolePage.includes("disabled={!composerEditable}") &&
|
||||
consolePage.includes('class:stop={workerRunning}') &&
|
||||
consolePage.includes("class:stop={workerRunning}") &&
|
||||
consolePage.includes('"Stop Worker"') &&
|
||||
consolePage.includes("disabled={composerSubmitDisabled}") &&
|
||||
!consolePage.includes("disabled={!inputReady || sending}") &&
|
||||
|
|
@ -502,7 +543,10 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
|||
new URL("../sidebar/sidebar.css", import.meta.url),
|
||||
);
|
||||
const workspaceLayout = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/+layout.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const workspaceLayoutLoad = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/+layout.ts", import.meta.url),
|
||||
|
|
@ -562,7 +606,9 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
|||
assert(
|
||||
workspaceLayout.includes("{#snippet workspaceSidebar()}") &&
|
||||
workspaceLayout.includes("WorkspaceSidebar") &&
|
||||
workspaceLayout.includes("<SidebarOverride sidebar={workspaceSidebar} />") &&
|
||||
workspaceLayout.includes(
|
||||
"<SidebarOverride sidebar={workspaceSidebar} />",
|
||||
) &&
|
||||
workspaceLayoutLoad.includes("params.workspaceId") &&
|
||||
workspaceLayoutLoad.includes("workspaceApiPath(workspaceId"),
|
||||
"Workspace layout should load workspace data and register a WorkspaceSidebar snippet",
|
||||
|
|
@ -592,7 +638,8 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
|||
"SidebarOverride should register and clean up the child-provided sidebar snippet",
|
||||
);
|
||||
assert(
|
||||
rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'),
|
||||
rootLayoutLoad.includes('"/account"') &&
|
||||
rootLayoutLoad.includes('"/login/device"'),
|
||||
"Root layout should not redirect account and device-login public routes to a workspace",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import {
|
|||
SETTINGS_SECTIONS,
|
||||
settingsSectionHref,
|
||||
} from "./model.ts";
|
||||
import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from "./profile-routes.ts";
|
||||
import {
|
||||
profileSourceTreeSettingsHref,
|
||||
virtualProfilePathForCreate,
|
||||
} from "./profile-routes.ts";
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void): void;
|
||||
|
|
@ -104,7 +107,6 @@ Deno.test("diagnostic labels preserve severity and code", () => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Deno.test("profile source tree routes use encoded scoped ids", () => {
|
||||
const href = profileSourceTreeSettingsHref("workspace a", "project/tree");
|
||||
assert(
|
||||
|
|
@ -115,7 +117,18 @@ Deno.test("profile source tree routes use encoded scoped ids", () => {
|
|||
});
|
||||
|
||||
Deno.test("profile source create paths are normalized to virtual profile paths", () => {
|
||||
assert(virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl", "bare file names are scoped");
|
||||
assert(virtualProfilePathForCreate("profiles/alpha.dcdl") === "profiles/alpha.dcdl", "virtual paths are preserved");
|
||||
assert(virtualProfilePathForCreate("project:profiles/alpha.dcdl") === "project:profiles/alpha.dcdl", "safe virtual namespaces are preserved");
|
||||
assert(
|
||||
virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl",
|
||||
"bare file names are scoped",
|
||||
);
|
||||
assert(
|
||||
virtualProfilePathForCreate("profiles/alpha.dcdl") ===
|
||||
"profiles/alpha.dcdl",
|
||||
"virtual paths are preserved",
|
||||
);
|
||||
assert(
|
||||
virtualProfilePathForCreate("project:profiles/alpha.dcdl") ===
|
||||
"project:profiles/alpha.dcdl",
|
||||
"safe virtual namespaces are preserved",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
workspaceApiJson,
|
||||
workspaceApiJsonWithBody,
|
||||
} from "../api/http";
|
||||
import { workspaceApiJson, workspaceApiJsonWithBody } from "../api/http";
|
||||
import type {
|
||||
ProfileSettingsMutationResponse,
|
||||
ProfileSettingsResponse,
|
||||
|
|
@ -124,7 +121,9 @@ export function fetchProfileSourceTree(
|
|||
sourceTreeId: string,
|
||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
||||
return workspaceApiJson(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}`,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
||||
encodeURIComponent(sourceTreeId)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +133,9 @@ export function fetchProfileTreeFile(
|
|||
path: string,
|
||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
||||
return workspaceApiJson(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file?path=${encodeURIComponent(path)}`,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
||||
encodeURIComponent(sourceTreeId)
|
||||
}/file?path=${encodeURIComponent(path)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +145,9 @@ export function writeProfileTreeFile(
|
|||
request: { path: string; content: string; revision?: string | null },
|
||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
||||
return workspaceApiJsonWithBody(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file`,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
||||
encodeURIComponent(sourceTreeId)
|
||||
}/file`,
|
||||
{ method: "PUT", body: JSON.stringify(request) },
|
||||
);
|
||||
}
|
||||
|
|
@ -155,7 +158,9 @@ export function deleteProfileTreeFile(
|
|||
request: { path: string; revision: string },
|
||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
||||
return workspaceApiJsonWithBody(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file`,
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
||||
encodeURIComponent(sourceTreeId)
|
||||
}/file`,
|
||||
{ method: "DELETE", body: JSON.stringify(request) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@ export function profileSettingsHref(workspaceId: string): string {
|
|||
return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`;
|
||||
}
|
||||
|
||||
export function profileSourceTreeSettingsHref(workspaceId: string, sourceTreeId: string): string {
|
||||
return `${profileSettingsHref(workspaceId)}/trees/${encodeURIComponent(sourceTreeId)}`;
|
||||
export function profileSourceTreeSettingsHref(
|
||||
workspaceId: string,
|
||||
sourceTreeId: string,
|
||||
): string {
|
||||
return `${profileSettingsHref(workspaceId)}/trees/${
|
||||
encodeURIComponent(sourceTreeId)
|
||||
}`;
|
||||
}
|
||||
|
||||
export function virtualProfilePathForCreate(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) return trimmed;
|
||||
if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.startsWith("profiles/")) return trimmed;
|
||||
return `profiles/${trimmed}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ function repositories(
|
|||
}
|
||||
|
||||
Deno.test("repository nav does not invent main for an empty registry", () => {
|
||||
const projection = projectRepositoryNav({
|
||||
const projection = projectRepositoryNav(
|
||||
{
|
||||
workspace_id: "workspace-1",
|
||||
items: [],
|
||||
source: "workspace_backend_config",
|
||||
|
|
@ -36,7 +37,10 @@ Deno.test("repository nav does not invent main for an empty registry", () => {
|
|||
message: "No repositories configured",
|
||||
},
|
||||
],
|
||||
}, "/w/workspace-1", "workspace-1");
|
||||
},
|
||||
"/w/workspace-1",
|
||||
"workspace-1",
|
||||
);
|
||||
|
||||
assertEquals(projection.count, 0);
|
||||
assertEquals(projection.items, []);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ export function projectRepositoryNav(
|
|||
count: summaries.length,
|
||||
diagnostics: repositories?.diagnostics ?? [],
|
||||
items: summaries.map((repository) => {
|
||||
const href = workspaceRoute(workspaceId, `/repositories/${encodeURIComponent(repository.id)}`);
|
||||
const href = workspaceRoute(
|
||||
workspaceId,
|
||||
`/repositories/${encodeURIComponent(repository.id)}`,
|
||||
);
|
||||
return {
|
||||
id: repository.id,
|
||||
title: repository.display_name || repository.id,
|
||||
|
|
|
|||
|
|
@ -319,12 +319,12 @@ export type RepositoryLogResponse = {
|
|||
};
|
||||
|
||||
export type MemoryCandidateKind =
|
||||
| 'preference'
|
||||
| 'working_assumption'
|
||||
| 'constraint'
|
||||
| 'decision'
|
||||
| 'open_question'
|
||||
| 'lesson';
|
||||
| "preference"
|
||||
| "working_assumption"
|
||||
| "constraint"
|
||||
| "decision"
|
||||
| "open_question"
|
||||
| "lesson";
|
||||
|
||||
export type MemorySourceRef = {
|
||||
segment_id: string;
|
||||
|
|
@ -387,7 +387,11 @@ export type TicketSummary = {
|
|||
updated_at?: string | null;
|
||||
queued_by?: string | null;
|
||||
queued_at?: string | null;
|
||||
workspace_action_priority?: 'ready_for_queue' | 'active_work' | 'background' | null;
|
||||
workspace_action_priority?:
|
||||
| "ready_for_queue"
|
||||
| "active_work"
|
||||
| "background"
|
||||
| null;
|
||||
record_source?: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,17 +35,23 @@ export function defaultWorkerLaunchForm(
|
|||
const preferredProfile =
|
||||
options?.profiles.find((candidate) => candidate.id === "builtin:coder") ??
|
||||
options?.profiles[0];
|
||||
const availableWorkingDirectories = options?.working_directories.filter((directory) =>
|
||||
const availableWorkingDirectories =
|
||||
options?.working_directories.filter((directory) =>
|
||||
directory.status === "active" &&
|
||||
directory.cleanliness === "clean" &&
|
||||
directory.primary_worker_id == null &&
|
||||
directory.occupied_by == null
|
||||
) ?? [];
|
||||
const selectedRuntime = current.runtime_id
|
||||
? options?.runtimes.find((runtime) => runtime.runtime_id === current.runtime_id)
|
||||
? options?.runtimes.find((runtime) =>
|
||||
runtime.runtime_id === current.runtime_id
|
||||
)
|
||||
: preferredRuntime;
|
||||
const workdirlessRuntime = selectedRuntime?.working_directory_required === false;
|
||||
const preferredWorkingDirectory = workdirlessRuntime ? undefined : availableWorkingDirectories[0];
|
||||
const workdirlessRuntime =
|
||||
selectedRuntime?.working_directory_required === false;
|
||||
const preferredWorkingDirectory = workdirlessRuntime
|
||||
? undefined
|
||||
: availableWorkingDirectories[0];
|
||||
const preferredRepository =
|
||||
options?.repositories.find((repository) =>
|
||||
repository.id === current.working_directory_repository_id
|
||||
|
|
@ -60,7 +66,8 @@ export function defaultWorkerLaunchForm(
|
|||
? current.profile
|
||||
: preferredProfile?.id || "",
|
||||
initial_text: current.initial_text,
|
||||
working_directory_id: !workdirlessRuntime && availableWorkingDirectories.some(
|
||||
working_directory_id:
|
||||
!workdirlessRuntime && availableWorkingDirectories.some(
|
||||
(directory) =>
|
||||
directory.working_directory_id === current.working_directory_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types";
|
||||
import type {
|
||||
RepositoryListResponse,
|
||||
WorkspaceResponse,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, params }) => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { loadJson, workspaceApiPath } from '$lib/workspace/api/http';
|
||||
import type { MemoryStagingListResponse } from '$lib/workspace/sidebar/types';
|
||||
import type { PageLoad } from './$types';
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { MemoryStagingListResponse } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
staging: await loadJson<MemoryStagingListResponse>(
|
||||
fetch,
|
||||
`${workspaceApiPath(params.workspaceId, '/memory/staging')}?limit=200`,
|
||||
`${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const objectiveId = params.objectiveId;
|
||||
const [objectives, objective] = await Promise.all([
|
||||
loadJson<ObjectiveListResponse>(fetch, apiPath('/objectives')),
|
||||
loadJson<ObjectiveListResponse>(fetch, apiPath("/objectives")),
|
||||
loadJson<ObjectiveDetail>(
|
||||
fetch,
|
||||
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export function load(
|
||||
{ params }: { params: { workspaceId: string; runtimeId: string; workerId: string } },
|
||||
{ params }: {
|
||||
params: { workspaceId: string; runtimeId: string; workerId: string };
|
||||
},
|
||||
) {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { PageLoad } from './$types';
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = ({ params }) => ({
|
||||
sourceTreeId: params.sourceTreeId,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import type { PageLoad } from "./$types";
|
|||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const runtimeId = params.runtimeId;
|
||||
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
|
||||
loadJson<ListResponse<Runtime>>(fetch, workspaceApiPath(params.workspaceId, "/runtimes")),
|
||||
loadJson<ListResponse<Runtime>>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/runtimes"),
|
||||
),
|
||||
loadJson<BrowserWorkingDirectoryListResponse>(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
|
|
@ -20,7 +23,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||
),
|
||||
loadJson<RuntimeCleanupPlanResponse>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ export const load = (async ({ fetch, params }) => {
|
|||
const ticketId = params.ticketId;
|
||||
const ticket = await loadJson<TicketDetail>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, `/tickets/${encodeURIComponent(ticketId)}`),
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/tickets/${encodeURIComponent(ticketId)}`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { ListResponse, RuntimeCleanupPlanResponse, Worker } from "$lib/workspace/sidebar/types";
|
||||
import type {
|
||||
ListResponse,
|
||||
RuntimeCleanupPlanResponse,
|
||||
Worker,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
|
|
@ -7,12 +11,17 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/workers"),
|
||||
);
|
||||
const runtimeIds = Array.from(new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []));
|
||||
const runtimeIds = Array.from(
|
||||
new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []),
|
||||
);
|
||||
const cleanupPlanEntries = await Promise.all(
|
||||
runtimeIds.map(async (runtimeId) => {
|
||||
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
||||
),
|
||||
);
|
||||
return [runtimeId, cleanupPlan] as const;
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
pages: "build",
|
||||
assets: "build",
|
||||
fallback: "index.html",
|
||||
precompress: false,
|
||||
strict: true
|
||||
})
|
||||
}
|
||||
strict: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
|
||||
server: {
|
||||
allowedHosts: ['develop.hareworks.net'],
|
||||
watch: { ignored: ['**/.tmp*', '**/.tmp*/**'] },
|
||||
allowedHosts: ["develop.hareworks.net"],
|
||||
watch: { ignored: ["**/.tmp*", "**/.tmp*/**"] },
|
||||
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8787',
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:8787",
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user