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.
|
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
|
## Development
|
||||||
|
|
||||||
|
|
@ -22,7 +23,9 @@ cd web/workspace
|
||||||
deno task dev
|
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:
|
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
|
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
|
## Static build
|
||||||
|
|
||||||
|
|
@ -40,7 +46,8 @@ Build the SPA:
|
||||||
deno task build
|
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
|
## Checks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
|
||||||
|
|
@ -12,112 +12,294 @@ export type WorkerStatus = "idle" | "running" | "paused";
|
||||||
|
|
||||||
export type TurnResult = "finished" | "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 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 Permission = "read" | "write";
|
||||||
|
|
||||||
export type InFlightToolCallState = "pending" | "streaming_args" | "done";
|
export type InFlightToolCallState = "pending" | "streaming_args" | "done";
|
||||||
|
|
||||||
export type ScopeRule = {
|
export type ScopeRule = {
|
||||||
/**
|
/**
|
||||||
* Target path. Must be absolute by the time a `Scope` is built from
|
* Target path. Must be absolute by the time a `Scope` is built from
|
||||||
* this rule — relative paths are resolved per-layer against the
|
* this rule — relative paths are resolved per-layer against the
|
||||||
* manifest file's directory (cwd for overlay layers) before cascade
|
* manifest file's directory (cwd for overlay layers) before cascade
|
||||||
* merge.
|
* merge.
|
||||||
*/
|
*/
|
||||||
target: string,
|
target: string;
|
||||||
/**
|
/**
|
||||||
* Permission level this rule grants (allow) or caps strictly below
|
* Permission level this rule grants (allow) or caps strictly below
|
||||||
* (deny).
|
* (deny).
|
||||||
*/
|
*/
|
||||||
permission: Permission,
|
permission: Permission;
|
||||||
/**
|
/**
|
||||||
* When `false`, the rule only matches the target itself and its
|
* When `false`, the rule only matches the target itself and its
|
||||||
* direct children. Defaults to `true`.
|
* 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;
|
||||||
* Model context window in tokens. Always filled by the Worker greeting.
|
cwd: string;
|
||||||
*/
|
provider: string;
|
||||||
context_window: number,
|
model: string;
|
||||||
/**
|
scope_summary: string;
|
||||||
* Estimated current session context tokens at connect time.
|
tools: Array<string>;
|
||||||
*/
|
/**
|
||||||
context_tokens: number, };
|
* Model context window in tokens. Always filled by the Worker greeting.
|
||||||
|
*/
|
||||||
|
context_window: number;
|
||||||
|
/**
|
||||||
|
* Estimated current session context tokens at connect time.
|
||||||
|
*/
|
||||||
|
context_tokens: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Alert = { level: AlertLevel, source: AlertSource, message: string,
|
export type Alert = {
|
||||||
/**
|
level: AlertLevel;
|
||||||
* Milliseconds since the Unix epoch.
|
source: AlertSource;
|
||||||
*/
|
message: string;
|
||||||
timestamp_ms: number, };
|
/**
|
||||||
|
* Milliseconds since the Unix epoch.
|
||||||
|
*/
|
||||||
|
timestamp_ms: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type MemoryWorkerEvent = { worker: string, status: string, run_id: string, trigger: string, reason: string,
|
export type MemoryWorkerEvent = {
|
||||||
/**
|
worker: string;
|
||||||
* Human-readable compact form for actionbar rendering.
|
status: string;
|
||||||
*/
|
run_id: string;
|
||||||
message: string,
|
trigger: string;
|
||||||
/**
|
reason: string;
|
||||||
* Milliseconds since the Unix epoch.
|
/**
|
||||||
*/
|
* Human-readable compact form for actionbar rendering.
|
||||||
timestamp_ms: number, };
|
*/
|
||||||
|
message: string;
|
||||||
|
/**
|
||||||
|
* Milliseconds since the Unix epoch.
|
||||||
|
*/
|
||||||
|
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 }
|
||||||
* Sub-delegating Worker (= the sender itself).
|
| { "kind": "errored"; worker_name: string; message: string }
|
||||||
*/
|
| { "kind": "shut_down"; worker_name: string }
|
||||||
parent_worker: string,
|
| {
|
||||||
/**
|
"kind": "scope_sub_delegated";
|
||||||
* Name of the grandchild Worker.
|
/**
|
||||||
*/
|
* Sub-delegating Worker (= the sender itself).
|
||||||
sub_worker: string,
|
*/
|
||||||
/**
|
parent_worker: string;
|
||||||
* Unix-socket path where the grandchild is reachable.
|
/**
|
||||||
*/
|
* Name of the grandchild Worker.
|
||||||
sub_socket: string,
|
*/
|
||||||
/**
|
sub_worker: string;
|
||||||
* Scope delegated to the grandchild.
|
/**
|
||||||
*/
|
* Unix-socket path where the grandchild is reachable.
|
||||||
scope: Array<ScopeRule>, };
|
*/
|
||||||
|
sub_socket: string;
|
||||||
|
/**
|
||||||
|
* Scope delegated to the grandchild.
|
||||||
|
*/
|
||||||
|
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> } }
|
||||||
* The attempt that just failed. 1 origin.
|
| { "event": "system_item"; "data": { item: unknown } }
|
||||||
*/
|
| { "event": "invoke_start"; "data": { kind: InvokeKind } }
|
||||||
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,
|
| { "event": "turn_start"; "data": { turn: number } }
|
||||||
/**
|
| { "event": "turn_end"; "data": { turn: number; result: TurnResult } }
|
||||||
* Short human-readable summary. Always present; used by clients
|
| { "event": "llm_call_start"; "data": { llm_call: number } }
|
||||||
* that only want a 1-line rendering (e.g. collapsed views).
|
| { "event": "llm_call_end"; "data": { llm_call: number } }
|
||||||
*/
|
| {
|
||||||
summary: string,
|
"event": "llm_retry";
|
||||||
/**
|
"data": {
|
||||||
* Full tool output. Absent when the tool chose to return
|
llm_call: number;
|
||||||
* summary-only, or when the result was pruned.
|
/**
|
||||||
*/
|
* The attempt that just failed. 1 origin.
|
||||||
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,
|
*/
|
||||||
/**
|
failed_attempt: number;
|
||||||
* Unfinished model output that has already streamed in the current
|
max_attempts: number;
|
||||||
* run but is not yet represented by committed snapshot entries.
|
wait_ms: number;
|
||||||
*/
|
elapsed_ms: number;
|
||||||
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" };
|
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;
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
/**
|
||||||
|
* 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" };
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
export type WorkspaceAlert = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -32,7 +38,8 @@ export function pushWorkspaceAlert(
|
||||||
message: string,
|
message: string,
|
||||||
options: { title?: string; id?: string } = {},
|
options: { title?: string; id?: string } = {},
|
||||||
): 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 = [
|
||||||
...alerts.filter((alert) => alert.id !== id),
|
...alerts.filter((alert) => alert.id !== id),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
import {
|
import {
|
||||||
authenticationCredentialToJson,
|
authenticationCredentialToJson,
|
||||||
isPublicKeyCredential,
|
|
||||||
prepareLoginOptions,
|
|
||||||
prepareRegistrationOptions,
|
|
||||||
registrationCredentialToJson,
|
|
||||||
type DeviceApprovalResponse,
|
type DeviceApprovalResponse,
|
||||||
|
isPublicKeyCredential,
|
||||||
type PasskeyLoginOptionsResponse,
|
type PasskeyLoginOptionsResponse,
|
||||||
type PasskeyRegistrationOptionsResponse,
|
type PasskeyRegistrationOptionsResponse,
|
||||||
type PasskeyUserResponse,
|
type PasskeyUserResponse,
|
||||||
|
prepareLoginOptions,
|
||||||
|
prepareRegistrationOptions,
|
||||||
|
registrationCredentialToJson,
|
||||||
type WhoamiResponse,
|
type WhoamiResponse,
|
||||||
} from "./model";
|
} from "./model";
|
||||||
|
|
||||||
async function jsonOrThrow<T>(response: Response): Promise<T> {
|
async function jsonOrThrow<T>(response: Response): Promise<T> {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!response.ok) {
|
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);
|
return text ? JSON.parse(text) as T : (null as T);
|
||||||
}
|
}
|
||||||
|
|
@ -23,8 +25,12 @@ function browserOrigin(): string | null {
|
||||||
return globalThis.location?.origin ?? null;
|
return globalThis.location?.origin ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadWhoami(fetcher: typeof fetch = fetch): Promise<WhoamiResponse> {
|
export async function loadWhoami(
|
||||||
return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow<WhoamiResponse>);
|
fetcher: typeof fetch = fetch,
|
||||||
|
): Promise<WhoamiResponse> {
|
||||||
|
return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(
|
||||||
|
jsonOrThrow<WhoamiResponse>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerPasskey(
|
export async function registerPasskey(
|
||||||
|
|
@ -36,14 +42,20 @@ export async function registerPasskey(
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
credentials: "same-origin",
|
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>);
|
}).then(jsonOrThrow<PasskeyRegistrationOptionsResponse>);
|
||||||
|
|
||||||
const credential = await navigator.credentials.create({
|
const credential = await navigator.credentials.create({
|
||||||
publicKey: prepareRegistrationOptions(options),
|
publicKey: prepareRegistrationOptions(options),
|
||||||
});
|
});
|
||||||
if (!isPublicKeyCredential(credential)) {
|
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", {
|
return await fetcher("/api/auth/passkeys/registration/complete", {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ declare const Deno: {
|
||||||
|
|
||||||
function assertEquals<T>(actual: T, expected: T): void {
|
function assertEquals<T>(actual: T, expected: T): void {
|
||||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
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) {
|
if (buffer instanceof ArrayBuffer) {
|
||||||
return [...new Uint8Array(buffer)];
|
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", () => {
|
Deno.test("base64url helpers round-trip binary data", () => {
|
||||||
|
|
@ -43,13 +47,20 @@ Deno.test("prepareRegistrationOptions decodes binary public key fields", () => {
|
||||||
displayName: "Local User",
|
displayName: "Local User",
|
||||||
},
|
},
|
||||||
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
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.challenge), [1, 2, 3]);
|
||||||
assertEquals(bytes(options.user.id), [4, 5, 6]);
|
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", () => {
|
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",
|
challenge_id: "challenge-1",
|
||||||
public_key: {
|
public_key: {
|
||||||
challenge: "AQID" as unknown as BufferSource,
|
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.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 = {
|
export type PasskeyRegistrationOptionsResponse = {
|
||||||
challenge_id: string;
|
challenge_id: string;
|
||||||
public_key: PublicKeyCredentialCreationOptions | { publicKey: PublicKeyCredentialCreationOptions };
|
public_key: PublicKeyCredentialCreationOptions | {
|
||||||
|
publicKey: PublicKeyCredentialCreationOptions;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PasskeyLoginOptionsResponse = {
|
export type PasskeyLoginOptionsResponse = {
|
||||||
challenge_id: string;
|
challenge_id: string;
|
||||||
public_key: PublicKeyCredentialRequestOptions | { publicKey: PublicKeyCredentialRequestOptions };
|
public_key: PublicKeyCredentialRequestOptions | {
|
||||||
|
publicKey: PublicKeyCredentialRequestOptions;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PasskeyUserResponse = {
|
export type PasskeyUserResponse = {
|
||||||
|
|
@ -65,7 +69,10 @@ export function bufferToBase64Url(buffer: ArrayBuffer): string {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
let binary = "";
|
let binary = "";
|
||||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
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 {
|
function unwrapPublicKey<T>(value: T | { publicKey: T }): T {
|
||||||
|
|
@ -79,12 +86,16 @@ export function prepareRegistrationOptions(
|
||||||
options: PasskeyRegistrationOptionsResponse,
|
options: PasskeyRegistrationOptionsResponse,
|
||||||
): PublicKeyCredentialCreationOptions {
|
): PublicKeyCredentialCreationOptions {
|
||||||
const publicKey = structuredClone(unwrapPublicKey(options.public_key));
|
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 = {
|
||||||
...publicKey.user,
|
...publicKey.user,
|
||||||
id: base64UrlToBuffer(publicKey.user.id as unknown as string),
|
id: base64UrlToBuffer(publicKey.user.id as unknown as string),
|
||||||
};
|
};
|
||||||
publicKey.excludeCredentials = publicKey.excludeCredentials?.map((credential) => ({
|
publicKey.excludeCredentials = publicKey.excludeCredentials?.map((
|
||||||
|
credential,
|
||||||
|
) => ({
|
||||||
...credential,
|
...credential,
|
||||||
id: base64UrlToBuffer(credential.id as unknown as string),
|
id: base64UrlToBuffer(credential.id as unknown as string),
|
||||||
}));
|
}));
|
||||||
|
|
@ -95,8 +106,12 @@ export function prepareLoginOptions(
|
||||||
options: PasskeyLoginOptionsResponse,
|
options: PasskeyLoginOptionsResponse,
|
||||||
): PublicKeyCredentialRequestOptions {
|
): PublicKeyCredentialRequestOptions {
|
||||||
const publicKey = structuredClone(unwrapPublicKey(options.public_key));
|
const publicKey = structuredClone(unwrapPublicKey(options.public_key));
|
||||||
publicKey.challenge = base64UrlToBuffer(publicKey.challenge as unknown as string);
|
publicKey.challenge = base64UrlToBuffer(
|
||||||
publicKey.allowCredentials = publicKey.allowCredentials?.map((credential) => ({
|
publicKey.challenge as unknown as string,
|
||||||
|
);
|
||||||
|
publicKey.allowCredentials = publicKey.allowCredentials?.map((
|
||||||
|
credential,
|
||||||
|
) => ({
|
||||||
...credential,
|
...credential,
|
||||||
id: base64UrlToBuffer(credential.id as unknown as string),
|
id: base64UrlToBuffer(credential.id as unknown as string),
|
||||||
}));
|
}));
|
||||||
|
|
@ -131,11 +146,15 @@ export function authenticationCredentialToJson(
|
||||||
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
|
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
|
||||||
authenticatorData: bufferToBase64Url(response.authenticatorData),
|
authenticatorData: bufferToBase64Url(response.authenticatorData),
|
||||||
signature: bufferToBase64Url(response.signature),
|
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";
|
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", () => {
|
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(
|
assertEquals(
|
||||||
shouldSubmitChatKey({ key: "Enter", metaKey: true, isComposing: true }, options),
|
shouldSubmitChatKey(
|
||||||
|
{ key: "Enter", metaKey: true, isComposing: true },
|
||||||
|
options,
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|
@ -69,7 +76,14 @@ Deno.test("shouldSubmitChatKey ignores IME composition and repeated Enter", () =
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("shouldSubmitChatKey supports enter submit mode", () => {
|
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");
|
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;
|
which?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeOptions(options: ChatSubmitOptions): NormalizedChatSubmitOptions {
|
function normalizeOptions(
|
||||||
|
options: ChatSubmitOptions,
|
||||||
|
): NormalizedChatSubmitOptions {
|
||||||
return {
|
return {
|
||||||
mode: "mod-enter",
|
mode: "mod-enter",
|
||||||
modKey: "auto",
|
modKey: "auto",
|
||||||
|
|
@ -47,7 +49,8 @@ function isApplePlatform(): boolean {
|
||||||
}
|
}
|
||||||
const platform = navigator.platform || "";
|
const platform = navigator.platform || "";
|
||||||
const userAgent = navigator.userAgent || "";
|
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" {
|
function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" {
|
||||||
|
|
@ -57,8 +60,13 @@ function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" {
|
||||||
return modKey;
|
return modKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isModPressed(event: ChatSubmitKeyEventLike, modKey: ChatSubmitModKey): boolean {
|
function isModPressed(
|
||||||
return resolveModKey(modKey) === "meta" ? event.metaKey === true : event.ctrlKey === true;
|
event: ChatSubmitKeyEventLike,
|
||||||
|
modKey: ChatSubmitModKey,
|
||||||
|
): boolean {
|
||||||
|
return resolveModKey(modKey) === "meta"
|
||||||
|
? event.metaKey === true
|
||||||
|
: event.ctrlKey === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldSubmitChatKey(
|
export function shouldSubmitChatKey(
|
||||||
|
|
@ -79,7 +87,10 @@ export function shouldSubmitChatKey(
|
||||||
return isModPressed(event, options.modKey);
|
return isModPressed(event, options.modKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chatSubmit(node: HTMLTextAreaElement, options: ChatSubmitOptions) {
|
export function chatSubmit(
|
||||||
|
node: HTMLTextAreaElement,
|
||||||
|
options: ChatSubmitOptions,
|
||||||
|
) {
|
||||||
let current = normalizeOptions(options);
|
let current = normalizeOptions(options);
|
||||||
let isComposing = false;
|
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", () => {
|
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> = {
|
const COMMANDS: Record<string, CommandSpec> = {
|
||||||
help: {
|
help: {
|
||||||
usage: ":help [command]",
|
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]",
|
usage: ":? [command]",
|
||||||
|
|
@ -49,7 +50,8 @@ const COMMANDS: Record<string, CommandSpec> = {
|
||||||
},
|
},
|
||||||
peer: {
|
peer: {
|
||||||
usage: ":peer <worker-name>",
|
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: {
|
system: {
|
||||||
usage: ":system <message>",
|
usage: ":system <message>",
|
||||||
|
|
@ -71,7 +73,9 @@ export function buildComposerRequest(value: string): ComposerCommandResult {
|
||||||
request: {
|
request: {
|
||||||
kind: "user",
|
kind: "user",
|
||||||
content,
|
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) {
|
if (argv.length > 0) {
|
||||||
return invalidUsage("compact");
|
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 "rewind":
|
||||||
case "rollback":
|
case "rollback":
|
||||||
if (argv.length > 0) {
|
if (argv.length > 0) {
|
||||||
return invalidUsage("rewind");
|
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":
|
case "peer":
|
||||||
if (argv.length !== 1) {
|
if (argv.length !== 1) {
|
||||||
return invalidUsage("peer");
|
return invalidUsage("peer");
|
||||||
|
|
@ -156,7 +168,10 @@ function helpCommand(argv: string[]): ComposerCommandResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidUsage(name: 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[] {
|
export function parseSigilSegments(input: string): Segment[] {
|
||||||
|
|
@ -178,7 +193,9 @@ export function parseSigilSegments(input: string): Segment[] {
|
||||||
if (cursor < input.length) {
|
if (cursor < input.length) {
|
||||||
segments.push({ kind: "text", content: input.slice(cursor) });
|
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 {
|
function sigilSegment(sigil: string, value: string): Segment {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Event } from "$lib/generated/protocol";
|
import type { Event } from "$lib/generated/protocol";
|
||||||
import {
|
import {
|
||||||
|
type ConsoleLine,
|
||||||
createConsoleProjector,
|
createConsoleProjector,
|
||||||
projectConsole,
|
projectConsole,
|
||||||
segmentsToText,
|
segmentsToText,
|
||||||
selectConsoleTimelineLines,
|
selectConsoleTimelineLines,
|
||||||
workerConsoleHref,
|
workerConsoleHref,
|
||||||
type ConsoleLine,
|
|
||||||
} from "./model.ts";
|
} from "./model.ts";
|
||||||
|
|
||||||
declare const Deno: {
|
declare const Deno: {
|
||||||
|
|
@ -68,8 +68,6 @@ Deno.test("workerConsoleHref encodes runtime and worker target authority", () =>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Deno.test("projectConsole projects visible protocol rows", () => {
|
Deno.test("projectConsole projects visible protocol rows", () => {
|
||||||
const projection = projectConsole([
|
const projection = projectConsole([
|
||||||
{
|
{
|
||||||
|
|
@ -192,7 +190,8 @@ Deno.test("projectConsole groups tool call lifecycle into one Call block", () =>
|
||||||
data: {
|
data: {
|
||||||
id: "call-1",
|
id: "call-1",
|
||||||
summary: "command completed",
|
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,
|
is_error: false,
|
||||||
},
|
},
|
||||||
} satisfies Event,
|
} satisfies Event,
|
||||||
|
|
@ -269,7 +268,10 @@ Deno.test("projectConsole caps default tool request and result previews", () =>
|
||||||
assertEquals(line.title, "Call · CustomTool");
|
assertEquals(line.title, "Call · CustomTool");
|
||||||
assertEquals(line.body.split("\n").length, 7);
|
assertEquals(line.body.split("\n").length, 7);
|
||||||
assert(line.body.includes("CustomTool — done"), "tool state should be shown");
|
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("out1"), "result preview should be shown");
|
||||||
assert(!line.body.includes("third"), "request preview should be capped");
|
assert(!line.body.includes("third"), "request preview should be capped");
|
||||||
assert(!line.body.includes("out3"), "result 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");
|
const [line] = projection.lines.filter((line) => line.kind === "tool");
|
||||||
assertEquals(line.title, "Call · Grep");
|
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("query: needle"), "Grep query should be shown");
|
||||||
assert(line.body.includes("hit1"), "first result should be shown");
|
assert(line.body.includes("hit1"), "first result should be shown");
|
||||||
assert(line.body.includes("hit5"), "fifth 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.status, "running");
|
||||||
assertEquals(
|
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:seed user:false",
|
||||||
"user:new user:false",
|
"user:new user:false",
|
||||||
|
|
@ -903,7 +910,9 @@ Deno.test("selectConsoleTimelineLines keeps all users and only last assistant pe
|
||||||
];
|
];
|
||||||
|
|
||||||
assertEquals(
|
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"],
|
["0:u1", "3:a2", "4:u2", "6:a4"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -940,7 +949,10 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
||||||
data: {
|
data: {
|
||||||
id: "write-rel",
|
id: "write-rel",
|
||||||
name: "Write",
|
name: "Write",
|
||||||
arguments: JSON.stringify({ file_path: "/repo/out.txt", content: "ok" }),
|
arguments: JSON.stringify({
|
||||||
|
file_path: "/repo/out.txt",
|
||||||
|
content: "ok",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
} satisfies Event,
|
} satisfies Event,
|
||||||
},
|
},
|
||||||
|
|
@ -1001,7 +1013,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
||||||
data: {
|
data: {
|
||||||
id: "glob-rel",
|
id: "glob-rel",
|
||||||
summary: "Found 2 file(s) matching **/*.rs",
|
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,
|
is_error: false,
|
||||||
},
|
},
|
||||||
} satisfies Event,
|
} 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");
|
assertEquals(bodies[0], "Read — 1 file read\n src/main.rs");
|
||||||
assert(
|
assert(
|
||||||
projection.lines[0].detail?.includes("from src/main.rs"),
|
projection.lines[0].detail?.includes("from src/main.rs"),
|
||||||
"Read summary detail path should be relative",
|
"Read summary detail path should be relative",
|
||||||
);
|
);
|
||||||
assert(
|
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",
|
"Write header and known result path should be relative",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -1055,7 +1072,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
bodies.some((body) =>
|
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",
|
"Grep should relativize line-start cwd paths and keep outside paths absolute",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
Event as ProtocolEvent,
|
|
||||||
Alert,
|
Alert,
|
||||||
|
Event as ProtocolEvent,
|
||||||
InFlightBlock,
|
InFlightBlock,
|
||||||
InFlightToolCallState,
|
InFlightToolCallState,
|
||||||
Segment,
|
Segment,
|
||||||
|
|
@ -148,7 +148,10 @@ export function emptyConsoleProjection(): ConsoleProjection {
|
||||||
export function projectConsole(
|
export function projectConsole(
|
||||||
events: ConsoleEventInput[] = [],
|
events: ConsoleEventInput[] = [],
|
||||||
): ConsoleProjection {
|
): ConsoleProjection {
|
||||||
const projection = events.reduce(applyProtocolEvent, emptyConsoleProjection());
|
const projection = events.reduce(
|
||||||
|
applyProtocolEvent,
|
||||||
|
emptyConsoleProjection(),
|
||||||
|
);
|
||||||
return projectVisibleConsole(projection);
|
return projectVisibleConsole(projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +174,9 @@ export function createConsoleProjector() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectVisibleConsole(projection: ConsoleProjection): ConsoleProjection {
|
function projectVisibleConsole(
|
||||||
|
projection: ConsoleProjection,
|
||||||
|
): ConsoleProjection {
|
||||||
return {
|
return {
|
||||||
...projection,
|
...projection,
|
||||||
lines: aggregateReadToolLines(projection.lines),
|
lines: aggregateReadToolLines(projection.lines),
|
||||||
|
|
@ -302,7 +307,9 @@ export function applyProtocolEvent(
|
||||||
next.status = event.data.status;
|
next.status = event.data.status;
|
||||||
break;
|
break;
|
||||||
case "segment_rotated":
|
case "segment_rotated":
|
||||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [event.data.entry], next.cwd);
|
next.lines = snapshotLinesFromEntries(envelope.eventId, [
|
||||||
|
event.data.entry,
|
||||||
|
], next.cwd);
|
||||||
break;
|
break;
|
||||||
case "invoke_start":
|
case "invoke_start":
|
||||||
case "turn_start":
|
case "turn_start":
|
||||||
|
|
@ -730,7 +737,9 @@ function readDetail(toolCall: ToolCallView): string {
|
||||||
`state: ${stateSuffix(toolCall.state)}`,
|
`state: ${stateSuffix(toolCall.state)}`,
|
||||||
`path: ${readPath(toolCall)}`,
|
`path: ${readPath(toolCall)}`,
|
||||||
toolCall.summary
|
toolCall.summary
|
||||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
? `summary: ${
|
||||||
|
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)
|
||||||
|
}`
|
||||||
: undefined,
|
: undefined,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -902,7 +911,9 @@ function toolCallDetail(toolCall: ToolCallView): string {
|
||||||
`id: ${toolCall.id}`,
|
`id: ${toolCall.id}`,
|
||||||
`state: ${stateSuffix(toolCall.state)}`,
|
`state: ${stateSuffix(toolCall.state)}`,
|
||||||
toolCall.summary
|
toolCall.summary
|
||||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
? `summary: ${
|
||||||
|
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)
|
||||||
|
}`
|
||||||
: undefined,
|
: undefined,
|
||||||
argsText(toolCall) ? `arguments:\n${argsText(toolCall)}` : undefined,
|
argsText(toolCall) ? `arguments:\n${argsText(toolCall)}` : undefined,
|
||||||
]);
|
]);
|
||||||
|
|
@ -1155,7 +1166,8 @@ function applyExtensionEntry(
|
||||||
}
|
}
|
||||||
const blockId = stringField(payload, "block_id") || "compact";
|
const blockId = stringField(payload, "block_id") || "compact";
|
||||||
const state = stringField(payload, "state") || "running";
|
const state = stringField(payload, "state") || "running";
|
||||||
const message = stringField(payload, "message") || compactMessageForState(state, payload);
|
const message = stringField(payload, "message") ||
|
||||||
|
compactMessageForState(state, payload);
|
||||||
upsertStatusLine(
|
upsertStatusLine(
|
||||||
projection,
|
projection,
|
||||||
blockId,
|
blockId,
|
||||||
|
|
@ -1204,26 +1216,38 @@ function applyLoggedItem(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "reasoning": {
|
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) {
|
if (text) {
|
||||||
projection.lines.push(line(eventId, "thinking", "Thought", text));
|
projection.lines.push(line(eventId, "thinking", "Thought", text));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tool_call":
|
case "tool_call":
|
||||||
upsertToolCall(projection, eventId, stringField(item, "call_id") ?? eventId, {
|
upsertToolCall(
|
||||||
name: stringField(item, "name") ?? "Tool",
|
projection,
|
||||||
arguments: stringField(item, "arguments") ?? "",
|
eventId,
|
||||||
argsStream: stringField(item, "arguments") ?? "",
|
stringField(item, "call_id") ?? eventId,
|
||||||
state: "running",
|
{
|
||||||
});
|
name: stringField(item, "name") ?? "Tool",
|
||||||
|
arguments: stringField(item, "arguments") ?? "",
|
||||||
|
argsStream: stringField(item, "arguments") ?? "",
|
||||||
|
state: "running",
|
||||||
|
},
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
attachToolResult(projection, eventId, stringField(item, "call_id") ?? eventId, {
|
attachToolResult(
|
||||||
summary: stringField(item, "summary") ?? "",
|
projection,
|
||||||
output: stringField(item, "content"),
|
eventId,
|
||||||
isError: item["is_error"] === true,
|
stringField(item, "call_id") ?? eventId,
|
||||||
});
|
{
|
||||||
|
summary: stringField(item, "summary") ?? "",
|
||||||
|
output: stringField(item, "content"),
|
||||||
|
isError: item["is_error"] === true,
|
||||||
|
},
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("workspace app css uses bundled UI fonts", async () => {
|
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(
|
assert(
|
||||||
appCss.includes("gen-interface-jp/400.css") &&
|
appCss.includes("gen-interface-jp/400.css") &&
|
||||||
appCss.includes("gen-interface-jp/500.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 () => {
|
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(
|
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(
|
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(
|
const accountPage = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/account/+page.svelte", import.meta.url),
|
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("runtimeSettingsHref") &&
|
||||||
workspacePage.includes("workersHref") &&
|
workspacePage.includes("workersHref") &&
|
||||||
workspacePage.includes("workspaceRoute(workspaceId, '/tickets')") &&
|
workspacePage.includes("workspaceRoute(workspaceId, '/tickets')") &&
|
||||||
workspacePage.includes("workspaceRoute(workspaceId, '/settings/runtimes')") &&
|
workspacePage.includes(
|
||||||
|
"workspaceRoute(workspaceId, '/settings/runtimes')",
|
||||||
|
) &&
|
||||||
workspacePage.includes("workspaceRoute(workspaceId, '/workers')"),
|
workspacePage.includes("workspaceRoute(workspaceId, '/workers')"),
|
||||||
"top workspace page should link to Tickets, Runtime Inventory under Settings, and the Workers page",
|
"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("workerConsoleHref(worker, data.workspaceId)") &&
|
||||||
workersPage.includes('<table class="workers-table">') &&
|
workersPage.includes('<table class="workers-table">') &&
|
||||||
workersPage.includes('class="icon-action"') &&
|
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",
|
"dedicated Workers page should expose a table, console link target, and icon actions per Worker",
|
||||||
);
|
);
|
||||||
assert(
|
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),
|
new URL("../sidebar/TicketsNavSection.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
const ticketsLoad = await Deno.readTextFile(
|
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(
|
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(
|
const ticketDetailLoad = await Deno.readTextFile(
|
||||||
new URL(
|
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",
|
"Tickets sidebar section should link to the workspace Tickets surface",
|
||||||
);
|
);
|
||||||
assert(
|
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("Notion-style filtering and sorting") &&
|
||||||
ticketsPage.includes("toggleSort('updated_at')") &&
|
ticketsPage.includes("toggleSort('updated_at')") &&
|
||||||
ticketsPage.includes("bind:value={visibilityFilter}") &&
|
ticketsPage.includes("bind:value={visibilityFilter}") &&
|
||||||
ticketsPage.includes("sortKey = $state<SortKey>('panel')") &&
|
ticketsPage.includes("sortKey = $state<SortKey>('panel')") &&
|
||||||
ticketsPage.includes("workspace_action_priority") &&
|
ticketsPage.includes("workspace_action_priority") &&
|
||||||
ticketsPage.includes("bind:value={stateFilter}") &&
|
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",
|
"Tickets list should read the workspace-scoped Ticket API and expose sortable/filterable table links",
|
||||||
);
|
);
|
||||||
assert(
|
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),
|
new URL("../sidebar/MemoryNavSection.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
const memoryLoad = await Deno.readTextFile(
|
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(
|
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(
|
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",
|
"Memory sidebar section should link to the workspace Memory Staging surface",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
memoryLoad.includes("workspaceApiPath(params.workspaceId, '/memory/staging')") &&
|
memoryLoad.includes("workspaceApiPath(params.workspaceId") &&
|
||||||
|
memoryLoad.includes('"/memory/staging"') &&
|
||||||
memoryPage.includes("Memory Staging") &&
|
memoryPage.includes("Memory Staging") &&
|
||||||
memoryPage.includes("Workspace Server memory authority") &&
|
memoryPage.includes("Workspace Server memory authority") &&
|
||||||
memoryPage.includes("data.staging.data.invalid_count") &&
|
memoryPage.includes("data.staging.data.invalid_count") &&
|
||||||
|
|
@ -236,7 +265,9 @@ Deno.test("Worker Console renders markdown only for message rows", async () => {
|
||||||
assert(
|
assert(
|
||||||
consoleLine.includes("function shouldRenderMarkdown") &&
|
consoleLine.includes("function shouldRenderMarkdown") &&
|
||||||
consoleLine.includes("item.kind === 'tool'") &&
|
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("{:else if shouldRenderMarkdown(item)}") &&
|
||||||
consoleLine.includes("<RichMarkdown text={item.body || '—'} />"),
|
consoleLine.includes("<RichMarkdown text={item.body || '—'} />"),
|
||||||
"Console should keep markdown rendering to user/assistant/system message bodies and render tool text literally",
|
"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(
|
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("{#each item.diff as diffLine}") &&
|
||||||
consoleLine.includes("class={`diff-line ${diffLine.kind}`}") &&
|
consoleLine.includes("class={`diff-line ${diffLine.kind}`}") &&
|
||||||
!consoleLine.includes('<pre class="console-diff"'),
|
!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") &&
|
consolePage.includes("jumpToTimelineMark") &&
|
||||||
consoleLine.includes("data-console-line-id={item.id}") &&
|
consoleLine.includes("data-console-line-id={item.id}") &&
|
||||||
consolePage.includes("class:timeline-open={timelineOpen}") &&
|
consolePage.includes("class:timeline-open={timelineOpen}") &&
|
||||||
consolePage.includes("class=\"timeline-fold\"") &&
|
consolePage.includes('class="timeline-fold"') &&
|
||||||
consolePage.includes("expanded={timelineOpen}") &&
|
consolePage.includes("expanded={timelineOpen}") &&
|
||||||
!consolePage.includes("{#if timelineOpen}") &&
|
!consolePage.includes("{#if timelineOpen}") &&
|
||||||
consolePage.includes("handleTimelineRailPointerDown") &&
|
consolePage.includes("handleTimelineRailPointerDown") &&
|
||||||
|
|
@ -301,17 +334,19 @@ Deno.test("Worker Console composer fits to content without manual resize", async
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
consolePage.includes("use:fitTextarea={{ value: draft, maxRows: 10 }}") &&
|
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("handleComposerShellClick") &&
|
||||||
consolePage.includes("bind:this={composerTextareaElement}") &&
|
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("scrollConsoleByPage") &&
|
||||||
consolePage.includes("class=\"composer-input-footer\"") &&
|
consolePage.includes('class="composer-input-footer"') &&
|
||||||
consolePage.includes("pointer-events: none") &&
|
consolePage.includes("pointer-events: none") &&
|
||||||
consolePage.includes("class=\"composer-footer-slot\"") &&
|
consolePage.includes('class="composer-footer-slot"') &&
|
||||||
consolePage.includes("class=\"composer-send-button\"") &&
|
consolePage.includes('class="composer-send-button"') &&
|
||||||
consolePage.includes("pointer-events: auto") &&
|
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('d="M8 6L12 2L16 6"') &&
|
||||||
consolePage.includes(".console-composer textarea") &&
|
consolePage.includes(".console-composer textarea") &&
|
||||||
consolePage.includes("resize: none") &&
|
consolePage.includes("resize: none") &&
|
||||||
|
|
@ -366,14 +401,16 @@ Deno.test("workspace Runtime inventory lives under Settings admin routes", async
|
||||||
assert(
|
assert(
|
||||||
!sidebar.includes("RuntimesNavSection") &&
|
!sidebar.includes("RuntimesNavSection") &&
|
||||||
settingsModel.includes('id: "runtime-inventory"') &&
|
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",
|
"Runtime inventory should be admin Settings navigation, not primary workspace sidebar navigation",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
runtimesPage.includes("Runtime Inventory") &&
|
runtimesPage.includes("Runtime Inventory") &&
|
||||||
runtimesPage.includes("Open workdirs") &&
|
runtimesPage.includes("Open workdirs") &&
|
||||||
runtimesPage.includes("runtimes-table") &&
|
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",
|
"Settings Runtime Inventory page should table Runtimes and link to each Runtime's workdirs",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -431,7 +468,9 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac
|
||||||
assert(
|
assert(
|
||||||
consolePage.includes("workspaceApiPath(workspaceId, path)") &&
|
consolePage.includes("workspaceApiPath(workspaceId, path)") &&
|
||||||
consolePage.includes("workerApiPath(") &&
|
consolePage.includes("workerApiPath(") &&
|
||||||
consolePage.includes("`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(") &&
|
consolePage.includes(
|
||||||
|
"`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(",
|
||||||
|
) &&
|
||||||
consolePage.includes("target.workerId"),
|
consolePage.includes("target.workerId"),
|
||||||
"Worker detail should use the scoped backend Worker detail API",
|
"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",
|
"target-change effect should load data without depending on manual refresh state reads",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
consolePage.includes('const workerRunning = $derived(workerState === "running");') &&
|
consolePage.includes(
|
||||||
|
'const workerRunning = $derived(workerState === "running");',
|
||||||
|
) &&
|
||||||
consolePage.includes(
|
consolePage.includes(
|
||||||
'const composerEditable = $derived(protocolState === "open" && !sending);',
|
'const composerEditable = $derived(protocolState === "open" && !sending);',
|
||||||
) &&
|
) &&
|
||||||
consolePage.includes('sendControl({ method: "cancel" }, "Stop")') &&
|
consolePage.includes('sendControl({ method: "cancel" }, "Stop")') &&
|
||||||
consolePage.includes("enabled: canSubmitDraft") &&
|
consolePage.includes("enabled: canSubmitDraft") &&
|
||||||
consolePage.includes("disabled={!composerEditable}") &&
|
consolePage.includes("disabled={!composerEditable}") &&
|
||||||
consolePage.includes('class:stop={workerRunning}') &&
|
consolePage.includes("class:stop={workerRunning}") &&
|
||||||
consolePage.includes('"Stop Worker"') &&
|
consolePage.includes('"Stop Worker"') &&
|
||||||
consolePage.includes("disabled={composerSubmitDisabled}") &&
|
consolePage.includes("disabled={composerSubmitDisabled}") &&
|
||||||
!consolePage.includes("disabled={!inputReady || sending}") &&
|
!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),
|
new URL("../sidebar/sidebar.css", import.meta.url),
|
||||||
);
|
);
|
||||||
const workspaceLayout = await Deno.readTextFile(
|
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(
|
const workspaceLayoutLoad = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/w/[workspaceId]/+layout.ts", import.meta.url),
|
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(
|
assert(
|
||||||
workspaceLayout.includes("{#snippet workspaceSidebar()}") &&
|
workspaceLayout.includes("{#snippet workspaceSidebar()}") &&
|
||||||
workspaceLayout.includes("WorkspaceSidebar") &&
|
workspaceLayout.includes("WorkspaceSidebar") &&
|
||||||
workspaceLayout.includes("<SidebarOverride sidebar={workspaceSidebar} />") &&
|
workspaceLayout.includes(
|
||||||
|
"<SidebarOverride sidebar={workspaceSidebar} />",
|
||||||
|
) &&
|
||||||
workspaceLayoutLoad.includes("params.workspaceId") &&
|
workspaceLayoutLoad.includes("params.workspaceId") &&
|
||||||
workspaceLayoutLoad.includes("workspaceApiPath(workspaceId"),
|
workspaceLayoutLoad.includes("workspaceApiPath(workspaceId"),
|
||||||
"Workspace layout should load workspace data and register a WorkspaceSidebar snippet",
|
"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",
|
"SidebarOverride should register and clean up the child-provided sidebar snippet",
|
||||||
);
|
);
|
||||||
assert(
|
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",
|
"Root layout should not redirect account and device-login public routes to a workspace",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import {
|
||||||
SETTINGS_SECTIONS,
|
SETTINGS_SECTIONS,
|
||||||
settingsSectionHref,
|
settingsSectionHref,
|
||||||
} from "./model.ts";
|
} from "./model.ts";
|
||||||
import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from "./profile-routes.ts";
|
import {
|
||||||
|
profileSourceTreeSettingsHref,
|
||||||
|
virtualProfilePathForCreate,
|
||||||
|
} from "./profile-routes.ts";
|
||||||
|
|
||||||
declare const Deno: {
|
declare const Deno: {
|
||||||
test(name: string, fn: () => void): void;
|
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", () => {
|
Deno.test("profile source tree routes use encoded scoped ids", () => {
|
||||||
const href = profileSourceTreeSettingsHref("workspace a", "project/tree");
|
const href = profileSourceTreeSettingsHref("workspace a", "project/tree");
|
||||||
assert(
|
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", () => {
|
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(
|
||||||
assert(virtualProfilePathForCreate("profiles/alpha.dcdl") === "profiles/alpha.dcdl", "virtual paths are preserved");
|
virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl",
|
||||||
assert(virtualProfilePathForCreate("project:profiles/alpha.dcdl") === "project:profiles/alpha.dcdl", "safe virtual namespaces are preserved");
|
"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 {
|
import { workspaceApiJson, workspaceApiJsonWithBody } from "../api/http";
|
||||||
workspaceApiJson,
|
|
||||||
workspaceApiJsonWithBody,
|
|
||||||
} from "../api/http";
|
|
||||||
import type {
|
import type {
|
||||||
ProfileSettingsMutationResponse,
|
ProfileSettingsMutationResponse,
|
||||||
ProfileSettingsResponse,
|
ProfileSettingsResponse,
|
||||||
|
|
@ -124,7 +121,9 @@ export function fetchProfileSourceTree(
|
||||||
sourceTreeId: string,
|
sourceTreeId: string,
|
||||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
): Promise<WorkspaceProfileSourceTreeResponse> {
|
||||||
return workspaceApiJson(
|
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,
|
path: string,
|
||||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
||||||
return workspaceApiJson(
|
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 },
|
request: { path: string; content: string; revision?: string | null },
|
||||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
||||||
return workspaceApiJsonWithBody(
|
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) },
|
{ method: "PUT", body: JSON.stringify(request) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +158,9 @@ export function deleteProfileTreeFile(
|
||||||
request: { path: string; revision: string },
|
request: { path: string; revision: string },
|
||||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
): Promise<WorkspaceProfileSourceTreeResponse> {
|
||||||
return workspaceApiJsonWithBody(
|
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) },
|
{ method: "DELETE", body: JSON.stringify(request) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@ export function profileSettingsHref(workspaceId: string): string {
|
||||||
return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`;
|
return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function profileSourceTreeSettingsHref(workspaceId: string, sourceTreeId: string): string {
|
export function profileSourceTreeSettingsHref(
|
||||||
return `${profileSettingsHref(workspaceId)}/trees/${encodeURIComponent(sourceTreeId)}`;
|
workspaceId: string,
|
||||||
|
sourceTreeId: string,
|
||||||
|
): string {
|
||||||
|
return `${profileSettingsHref(workspaceId)}/trees/${
|
||||||
|
encodeURIComponent(sourceTreeId)
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function virtualProfilePathForCreate(input: string): string {
|
export function virtualProfilePathForCreate(input: string): string {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) return "";
|
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;
|
if (trimmed.startsWith("profiles/")) return trimmed;
|
||||||
return `profiles/${trimmed}`;
|
return `profiles/${trimmed}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,22 @@ function repositories(
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("repository nav does not invent main for an empty registry", () => {
|
Deno.test("repository nav does not invent main for an empty registry", () => {
|
||||||
const projection = projectRepositoryNav({
|
const projection = projectRepositoryNav(
|
||||||
workspace_id: "workspace-1",
|
{
|
||||||
items: [],
|
workspace_id: "workspace-1",
|
||||||
source: "workspace_backend_config",
|
items: [],
|
||||||
diagnostics: [
|
source: "workspace_backend_config",
|
||||||
{
|
diagnostics: [
|
||||||
code: "repository_config_empty",
|
{
|
||||||
severity: "warning",
|
code: "repository_config_empty",
|
||||||
message: "No repositories configured",
|
severity: "warning",
|
||||||
},
|
message: "No repositories configured",
|
||||||
],
|
},
|
||||||
}, "/w/workspace-1", "workspace-1");
|
],
|
||||||
|
},
|
||||||
|
"/w/workspace-1",
|
||||||
|
"workspace-1",
|
||||||
|
);
|
||||||
|
|
||||||
assertEquals(projection.count, 0);
|
assertEquals(projection.count, 0);
|
||||||
assertEquals(projection.items, []);
|
assertEquals(projection.items, []);
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ export function projectRepositoryNav(
|
||||||
count: summaries.length,
|
count: summaries.length,
|
||||||
diagnostics: repositories?.diagnostics ?? [],
|
diagnostics: repositories?.diagnostics ?? [],
|
||||||
items: summaries.map((repository) => {
|
items: summaries.map((repository) => {
|
||||||
const href = workspaceRoute(workspaceId, `/repositories/${encodeURIComponent(repository.id)}`);
|
const href = workspaceRoute(
|
||||||
|
workspaceId,
|
||||||
|
`/repositories/${encodeURIComponent(repository.id)}`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
id: repository.id,
|
id: repository.id,
|
||||||
title: repository.display_name || repository.id,
|
title: repository.display_name || repository.id,
|
||||||
|
|
|
||||||
|
|
@ -1,249 +1,249 @@
|
||||||
@layer reset, tokens, base, layout, components;
|
@layer reset, tokens, base, layout, components;
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.sidebar-frame {
|
.sidebar-frame {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
width: clamp(220px, 20vw, 280px);
|
width: clamp(220px, 20vw, 280px);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--space-4) var(--space-3);
|
padding: var(--space-4) var(--space-3);
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.sidebar-frame.folded {
|
.sidebar-frame.folded {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-inline: var(--space-2);
|
padding-inline: var(--space-2);
|
||||||
}
|
}
|
||||||
.sidebar-frame-content,
|
.sidebar-frame-content,
|
||||||
.global-sidebar,
|
.global-sidebar,
|
||||||
.workspace-sidebar {
|
.workspace-sidebar {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.global-sidebar,
|
.global-sidebar,
|
||||||
.global-sidebar-section,
|
.global-sidebar-section,
|
||||||
.workspace-sidebar {
|
.workspace-sidebar {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.sidebar-control-row,
|
.sidebar-control-row,
|
||||||
.sidebar-actions-row {
|
.sidebar-actions-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
.sidebar-control-row {
|
.sidebar-control-row {
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
.sidebar-frame.folded .sidebar-control-row {
|
.sidebar-frame.folded .sidebar-control-row {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.sidebar-title-row {
|
.sidebar-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.workspace-label {
|
.workspace-label {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.workspace-name {
|
.workspace-name {
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--radius-soft);
|
border-radius: var(--radius-soft);
|
||||||
padding: 0.45rem 0.6rem;
|
padding: 0.45rem 0.6rem;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.workspace-status {
|
.workspace-status {
|
||||||
margin: var(--space-1) 0 0;
|
margin: var(--space-1) 0 0;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
.workspace-status.error,
|
.workspace-status.error,
|
||||||
.section-state.error,
|
.section-state.error,
|
||||||
.error {
|
.error {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
.sidebar-icon-button,
|
.sidebar-icon-button,
|
||||||
.sidebar-fold-button {
|
.sidebar-fold-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: var(--radius-soft);
|
border-radius: var(--radius-soft);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.sidebar-fold-button {
|
.sidebar-fold-button {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
.sidebar-icon-button:hover,
|
.sidebar-icon-button:hover,
|
||||||
.sidebar-icon-button:focus-visible,
|
.sidebar-icon-button:focus-visible,
|
||||||
.sidebar-fold-button:hover,
|
.sidebar-fold-button:hover,
|
||||||
.sidebar-fold-button:focus-visible {
|
.sidebar-fold-button:focus-visible {
|
||||||
background: var(--interactive-hover);
|
background: var(--interactive-hover);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
.sidebar-sections {
|
.sidebar-sections {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-5);
|
gap: var(--space-5);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.nav-section {
|
.nav-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
.section-heading-row,
|
.section-heading-row,
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
.section-heading-row h2,
|
.section-heading-row h2,
|
||||||
.section-header,
|
.section-header,
|
||||||
.sidebar-section-label {
|
.sidebar-section-label {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
letter-spacing: 0.11em;
|
letter-spacing: 0.11em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.section-heading-row h2,
|
.section-heading-row h2,
|
||||||
.sidebar-section-label {
|
.sidebar-section-label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.section-heading-link {
|
.section-heading-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.section-heading-link:hover,
|
.section-heading-link:hover,
|
||||||
.section-heading-link:focus-visible,
|
.section-heading-link:focus-visible,
|
||||||
.section-heading-link.active {
|
.section-heading-link.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.section-count {
|
.section-count {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.nav-list,
|
.nav-list,
|
||||||
.sidebar-list {
|
.sidebar-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.nav-item,
|
.nav-item,
|
||||||
.objective-link,
|
.objective-link,
|
||||||
.sidebar-link {
|
.sidebar-link {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-inline: calc(-1 * var(--space-2));
|
margin-inline: calc(-1 * var(--space-2));
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
border-radius: var(--radius-soft);
|
border-radius: var(--radius-soft);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: background-color 140ms ease, color 140ms ease;
|
transition: background-color 140ms ease, color 140ms ease;
|
||||||
}
|
}
|
||||||
a.nav-item:hover,
|
a.nav-item:hover,
|
||||||
a.nav-item:focus-visible,
|
a.nav-item:focus-visible,
|
||||||
a.objective-link:hover,
|
a.objective-link:hover,
|
||||||
a.objective-link:focus-visible,
|
a.objective-link:focus-visible,
|
||||||
a.sidebar-link:hover,
|
a.sidebar-link:hover,
|
||||||
a.sidebar-link:focus-visible {
|
a.sidebar-link:focus-visible {
|
||||||
background: var(--interactive-hover);
|
background: var(--interactive-hover);
|
||||||
}
|
}
|
||||||
a.nav-item.active,
|
a.nav-item.active,
|
||||||
a.objective-link.active,
|
a.objective-link.active,
|
||||||
a.sidebar-link.active {
|
a.sidebar-link.active {
|
||||||
background: var(--interactive-selected);
|
background: var(--interactive-selected);
|
||||||
}
|
}
|
||||||
a.nav-item.active .item-title,
|
a.nav-item.active .item-title,
|
||||||
a.objective-link.active .item-title,
|
a.objective-link.active .item-title,
|
||||||
a.sidebar-link.active {
|
a.sidebar-link.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.item-title,
|
.item-title,
|
||||||
.item-meta {
|
.item-meta {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.item-title {
|
.item-title {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
.item-meta {
|
.item-meta {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
.worker-nav-item {
|
.worker-nav-item {
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.worker-nav-item.disabled {
|
.worker-nav-item.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.62;
|
opacity: 0.62;
|
||||||
}
|
}
|
||||||
.worker-nav-item.disabled .item-title {
|
.worker-nav-item.disabled .item-title {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
.worker-title-row {
|
.worker-title-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
|
grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.worker-task-title {
|
.worker-task-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.sidebar-frame,
|
.sidebar-frame,
|
||||||
.sidebar-frame.folded {
|
.sidebar-frame.folded {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
width: auto;
|
width: auto;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -319,12 +319,12 @@ export type RepositoryLogResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MemoryCandidateKind =
|
export type MemoryCandidateKind =
|
||||||
| 'preference'
|
| "preference"
|
||||||
| 'working_assumption'
|
| "working_assumption"
|
||||||
| 'constraint'
|
| "constraint"
|
||||||
| 'decision'
|
| "decision"
|
||||||
| 'open_question'
|
| "open_question"
|
||||||
| 'lesson';
|
| "lesson";
|
||||||
|
|
||||||
export type MemorySourceRef = {
|
export type MemorySourceRef = {
|
||||||
segment_id: string;
|
segment_id: string;
|
||||||
|
|
@ -387,7 +387,11 @@ export type TicketSummary = {
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
queued_by?: string | null;
|
queued_by?: string | null;
|
||||||
queued_at?: 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;
|
record_source?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,23 @@ export function defaultWorkerLaunchForm(
|
||||||
const preferredProfile =
|
const preferredProfile =
|
||||||
options?.profiles.find((candidate) => candidate.id === "builtin:coder") ??
|
options?.profiles.find((candidate) => candidate.id === "builtin:coder") ??
|
||||||
options?.profiles[0];
|
options?.profiles[0];
|
||||||
const availableWorkingDirectories = options?.working_directories.filter((directory) =>
|
const availableWorkingDirectories =
|
||||||
directory.status === "active" &&
|
options?.working_directories.filter((directory) =>
|
||||||
directory.cleanliness === "clean" &&
|
directory.status === "active" &&
|
||||||
directory.primary_worker_id == null &&
|
directory.cleanliness === "clean" &&
|
||||||
directory.occupied_by == null
|
directory.primary_worker_id == null &&
|
||||||
) ?? [];
|
directory.occupied_by == null
|
||||||
|
) ?? [];
|
||||||
const selectedRuntime = current.runtime_id
|
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;
|
: preferredRuntime;
|
||||||
const workdirlessRuntime = selectedRuntime?.working_directory_required === false;
|
const workdirlessRuntime =
|
||||||
const preferredWorkingDirectory = workdirlessRuntime ? undefined : availableWorkingDirectories[0];
|
selectedRuntime?.working_directory_required === false;
|
||||||
|
const preferredWorkingDirectory = workdirlessRuntime
|
||||||
|
? undefined
|
||||||
|
: availableWorkingDirectories[0];
|
||||||
const preferredRepository =
|
const preferredRepository =
|
||||||
options?.repositories.find((repository) =>
|
options?.repositories.find((repository) =>
|
||||||
repository.id === current.working_directory_repository_id
|
repository.id === current.working_directory_repository_id
|
||||||
|
|
@ -60,12 +66,13 @@ export function defaultWorkerLaunchForm(
|
||||||
? current.profile
|
? current.profile
|
||||||
: preferredProfile?.id || "",
|
: preferredProfile?.id || "",
|
||||||
initial_text: current.initial_text,
|
initial_text: current.initial_text,
|
||||||
working_directory_id: !workdirlessRuntime && availableWorkingDirectories.some(
|
working_directory_id:
|
||||||
(directory) =>
|
!workdirlessRuntime && availableWorkingDirectories.some(
|
||||||
directory.working_directory_id === current.working_directory_id,
|
(directory) =>
|
||||||
)
|
directory.working_directory_id === current.working_directory_id,
|
||||||
? current.working_directory_id
|
)
|
||||||
: preferredWorkingDirectory?.working_directory_id || "",
|
? current.working_directory_id
|
||||||
|
: preferredWorkingDirectory?.working_directory_id || "",
|
||||||
working_directory_repository_id: current.working_directory_repository_id ||
|
working_directory_repository_id: current.working_directory_repository_id ||
|
||||||
preferredRepository?.id || "",
|
preferredRepository?.id || "",
|
||||||
working_directory_selector: current.working_directory_selector ||
|
working_directory_selector: current.working_directory_selector ||
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
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";
|
import type { LayoutLoad } from "./$types";
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ fetch, params }) => {
|
export const load: LayoutLoad = async ({ fetch, params }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { loadJson, workspaceApiPath } from '$lib/workspace/api/http';
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
import type { MemoryStagingListResponse } from '$lib/workspace/sidebar/types';
|
import type { MemoryStagingListResponse } from "$lib/workspace/sidebar/types";
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
return {
|
return {
|
||||||
workspaceId: params.workspaceId,
|
workspaceId: params.workspaceId,
|
||||||
staging: await loadJson<MemoryStagingListResponse>(
|
staging: await loadJson<MemoryStagingListResponse>(
|
||||||
fetch,
|
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 apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||||
const objectiveId = params.objectiveId;
|
const objectiveId = params.objectiveId;
|
||||||
const [objectives, objective] = await Promise.all([
|
const [objectives, objective] = await Promise.all([
|
||||||
loadJson<ObjectiveListResponse>(fetch, apiPath('/objectives')),
|
loadJson<ObjectiveListResponse>(fetch, apiPath("/objectives")),
|
||||||
loadJson<ObjectiveDetail>(
|
loadJson<ObjectiveDetail>(
|
||||||
fetch,
|
fetch,
|
||||||
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
|
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export function load(
|
export function load(
|
||||||
{ params }: { params: { workspaceId: string; runtimeId: string; workerId: string } },
|
{ params }: {
|
||||||
|
params: { workspaceId: string; runtimeId: string; workerId: string };
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
workspaceId: params.workspaceId,
|
workspaceId: params.workspaceId,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = ({ params }) => ({
|
export const load: PageLoad = ({ params }) => ({
|
||||||
sourceTreeId: params.sourceTreeId,
|
sourceTreeId: params.sourceTreeId,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ import type { PageLoad } from "./$types";
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
const runtimeId = params.runtimeId;
|
const runtimeId = params.runtimeId;
|
||||||
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
|
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>(
|
loadJson<BrowserWorkingDirectoryListResponse>(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(
|
workspaceApiPath(
|
||||||
|
|
@ -20,7 +23,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
),
|
),
|
||||||
loadJson<RuntimeCleanupPlanResponse>(
|
loadJson<RuntimeCleanupPlanResponse>(
|
||||||
fetch,
|
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 ticketId = params.ticketId;
|
||||||
const ticket = await loadJson<TicketDetail>(
|
const ticket = await loadJson<TicketDetail>(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, `/tickets/${encodeURIComponent(ticketId)}`),
|
workspaceApiPath(
|
||||||
|
params.workspaceId,
|
||||||
|
`/tickets/${encodeURIComponent(ticketId)}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
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";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
|
|
@ -7,12 +11,17 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, "/workers"),
|
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(
|
const cleanupPlanEntries = await Promise.all(
|
||||||
runtimeIds.map(async (runtimeId) => {
|
runtimeIds.map(async (runtimeId) => {
|
||||||
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
|
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
workspaceApiPath(
|
||||||
|
params.workspaceId,
|
||||||
|
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return [runtimeId, cleanupPlan] as const;
|
return [runtimeId, cleanupPlan] as const;
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import adapter from '@sveltejs/adapter-static';
|
import adapter from "@sveltejs/adapter-static";
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: 'build',
|
pages: "build",
|
||||||
assets: 'build',
|
assets: "build",
|
||||||
fallback: 'index.html',
|
fallback: "index.html",
|
||||||
precompress: false,
|
precompress: false,
|
||||||
strict: true
|
strict: true,
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit()],
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['develop.hareworks.net'],
|
allowedHosts: ["develop.hareworks.net"],
|
||||||
watch: { ignored: ['**/.tmp*', '**/.tmp*/**'] },
|
watch: { ignored: ["**/.tmp*", "**/.tmp*/**"] },
|
||||||
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
"/api": {
|
||||||
target: 'http://127.0.0.1:8787',
|
target: "http://127.0.0.1:8787",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true
|
ws: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user