feat: refine workspace console ui

This commit is contained in:
Keisuke Hirata 2026-07-15 22:43:56 +09:00
parent f9f1f85b07
commit 66abd53e51
No known key found for this signature in database
63 changed files with 406 additions and 212 deletions

View File

@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace-api/http.test.ts src/lib/workspace-console/chat-submit.test.ts src/lib/workspace-console/composer-command.test.ts src/lib/workspace-console/composer-completion.test.ts src/lib/workspace-console/markdown.test.ts src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts src/lib/workspace-sidebar/workers.test.ts src/lib/workspace-sidebar/worker-launch.test.ts src/lib/workspace-sidebar/repository-nav.test.ts", "test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/api/http.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts",
"build": "deno run -A npm:vite@7.2.7 build", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview" "preview": "deno run -A npm:vite@7.2.7 preview"
}, },

View File

@ -29,7 +29,7 @@ Deno.test("workspace route helpers scope browser routes and API by immutable wor
}); });
Deno.test("unscoped layout bootstraps then redirects instead of loading unscoped workspace data", async () => { Deno.test("unscoped layout bootstraps then redirects instead of loading unscoped workspace data", async () => {
const layout = await Deno.readTextFile(new URL("../../routes/+layout.ts", import.meta.url)); const layout = await Deno.readTextFile(new URL("./../../../routes/+layout.ts", import.meta.url));
assert( assert(
layout.includes('loadJson<WorkspaceResponse>(fetch, "/api/workspace")'), layout.includes('loadJson<WorkspaceResponse>(fetch, "/api/workspace")'),
"unscoped layout may use only the workspace-id bootstrap endpoint", "unscoped layout may use only the workspace-id bootstrap endpoint",
@ -44,7 +44,7 @@ Deno.test("unscoped layout bootstraps then redirects instead of loading unscoped
); );
const unscopedSettings = await Deno.readTextFile( const unscopedSettings = await Deno.readTextFile(
new URL("../../routes/settings/+page.svelte", import.meta.url), new URL("./../../../routes/settings/+page.svelte", import.meta.url),
); );
assert( assert(
unscopedSettings.includes("Redirecting to scoped settings") && unscopedSettings.includes("Redirecting to scoped settings") &&

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import RichMarkdown from '$lib/workspace-console/RichMarkdown.svelte'; import RichMarkdown from '$lib/workspace/console/RichMarkdown.svelte';
import type { ConsoleLine } from '$lib/workspace-console/model'; import type { ConsoleLine } from '$lib/workspace/console/model';
type Props = { type Props = {
item: ConsoleLine; item: ConsoleLine;
@ -73,8 +73,11 @@
<p class="console-plain-text">{item.body || '—'}</p> <p class="console-plain-text">{item.body || '—'}</p>
{/if} {/if}
{#if item.diff} {#if item.diff}
<pre class="console-diff" aria-label="Edit diff">{#each item.diff as diffLine} <div class="console-diff" role="group" aria-label="Edit diff">
<span class={`diff-line ${diffLine.kind}`}><span class="diff-gutter">{diffLine.oldNumber ?? ''}</span><span class="diff-gutter">{diffLine.newNumber ?? ''}</span><span class="diff-marker">{diffLine.kind === 'add' ? '+' : diffLine.kind === 'remove' ? '-' : ' '}</span><span class="diff-content">{diffLine.content}</span></span>{/each}</pre> {#each item.diff as diffLine}
<span class={`diff-line ${diffLine.kind}`}><span class="diff-gutter">{diffLine.oldNumber ?? ''}</span><span class="diff-gutter">{diffLine.newNumber ?? ''}</span><span class="diff-marker">{diffLine.kind === 'add' ? '+' : diffLine.kind === 'remove' ? '-' : ' '}</span><span class="diff-content">{diffLine.content}</span></span>
{/each}
</div>
{/if} {/if}
{#if item.detail} {#if item.detail}
<details class="message-detail"> <details class="message-detail">
@ -222,6 +225,7 @@
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 0.65rem; border-radius: 0.65rem;
color: var(--text); color: var(--text);
font-family: var(--font-mono);
font-size: 0.78rem; font-size: 0.78rem;
line-height: 1.45; line-height: 1.45;
margin: 0.6rem 0 0; margin: 0.6rem 0 0;

View File

@ -16,11 +16,19 @@
marks: TimelineMark[]; marks: TimelineMark[];
thumbStyle: string; thumbStyle: string;
axisStyle: string; axisStyle: string;
onRailClick: (event: MouseEvent) => void; expanded: boolean;
onRailPointerDown: (event: PointerEvent) => void;
onMarkClick: (mark: TimelineMark) => void; onMarkClick: (mark: TimelineMark) => void;
}; };
let { marks, thumbStyle, axisStyle, onRailClick, onMarkClick }: Props = $props(); let {
marks,
thumbStyle,
axisStyle,
expanded,
onRailPointerDown,
onMarkClick,
}: Props = $props();
</script> </script>
<aside class="console-timeline" aria-label="Console timeline"> <aside class="console-timeline" aria-label="Console timeline">
@ -28,8 +36,8 @@
<button <button
type="button" type="button"
class="timeline-rail" class="timeline-rail"
aria-label="Jump within console scrollback" aria-label="Scroll console"
onclick={onRailClick} onpointerdown={onRailPointerDown}
> >
<span class="timeline-thumb" style={thumbStyle}></span> <span class="timeline-thumb" style={thumbStyle}></span>
</button> </button>
@ -37,7 +45,7 @@
{#each marks as mark (mark.id)} {#each marks as mark (mark.id)}
<button <button
type="button" type="button"
class={`timeline-mark ${mark.kind}`} class={`timeline-mark ${mark.kind} ${expanded ? 'expanded' : 'folded'}`}
style={`top: ${mark.position}px`} style={`top: ${mark.position}px`}
aria-label={mark.detail ? `${mark.label}: ${mark.detail}` : mark.label} aria-label={mark.detail ? `${mark.label}: ${mark.detail}` : mark.label}
onclick={() => onMarkClick(mark)} onclick={() => onMarkClick(mark)}
@ -81,12 +89,14 @@
} }
.timeline-rail { .timeline-rail {
width: 0.35rem; width: 0.65rem;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: color-mix(in srgb, var(--line) 70%, transparent); background: color-mix(in srgb, var(--line) 70%, transparent);
cursor: pointer; cursor: ns-resize;
padding: 0; padding: 0;
touch-action: none;
user-select: none;
} }
.timeline-thumb { .timeline-thumb {
@ -97,6 +107,7 @@
border-radius: inherit; border-radius: inherit;
background: var(--tui-cyan); background: var(--tui-cyan);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--tui-cyan) 30%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--tui-cyan) 30%, transparent);
pointer-events: none;
} }
.timeline-marks { .timeline-marks {
@ -139,7 +150,7 @@
} }
.timeline-card { .timeline-card {
display: grid; display: none;
width: 10rem; width: 10rem;
min-width: 0; min-width: 0;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
@ -154,6 +165,10 @@
transform: none; transform: none;
} }
.timeline-mark.expanded .timeline-card {
display: grid;
}
.timeline-card span { .timeline-card span {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { markdownToHtml } from "$lib/workspace-console/markdown"; import { markdownToHtml } from "$lib/workspace/console/markdown";
type Props = { type Props = {
text: string; text: string;

View File

@ -5,7 +5,7 @@ import type {
InFlightToolCallState, InFlightToolCallState,
Segment, Segment,
} from "$lib/generated/protocol"; } from "$lib/generated/protocol";
import { workspaceRoute } from "$lib/workspace-api/http"; import { workspaceRoute } from "$lib/workspace/api/http";
export type ConsoleLineKind = export type ConsoleLineKind =
| "user" | "user"

View File

@ -11,19 +11,19 @@ function assert(condition: unknown, message: string): asserts condition {
Deno.test("workspace Worker list lives on the dedicated Workers page", async () => { Deno.test("workspace Worker list lives on the dedicated Workers page", async () => {
const workspacePage = await Deno.readTextFile( const workspacePage = await Deno.readTextFile(
new URL("./../../routes/w/[workspaceId]/+page.svelte", import.meta.url), new URL("./../../../routes/w/[workspaceId]/+page.svelte", import.meta.url),
); );
const workersPage = await Deno.readTextFile( const workersPage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/workers/+page.svelte", "./../../../routes/w/[workspaceId]/workers/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
const workersNav = await Deno.readTextFile( const workersNav = await Deno.readTextFile(
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url), new URL("../sidebar/WorkersNavSection.svelte", import.meta.url),
); );
const sidebar = await Deno.readTextFile( const sidebar = await Deno.readTextFile(
new URL("../workspace-sidebar/WorkspaceSidebar.svelte", import.meta.url), new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
); );
assert( assert(
@ -59,7 +59,7 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
Deno.test("Worker Console uses protocol observation events without transcript fetch", async () => { Deno.test("Worker Console uses protocol observation events without transcript fetch", async () => {
const consolePage = await Deno.readTextFile( const consolePage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
@ -94,10 +94,24 @@ Deno.test("Worker Console renders markdown only for message rows", async () => {
); );
}); });
Deno.test("Worker Console renders Edit diffs without preformatted template gaps", async () => {
const consoleLine = await Deno.readTextFile(
new URL("./ConsoleLineItem.svelte", import.meta.url),
);
assert(
consoleLine.includes('<div class="console-diff" role="group" aria-label="Edit diff">') &&
consoleLine.includes("{#each item.diff as diffLine}") &&
consoleLine.includes("class={`diff-line ${diffLine.kind}`}") &&
!consoleLine.includes('<pre class="console-diff"'),
"Edit diff rows should not be wrapped in a pre element that preserves template whitespace as blank lines",
);
});
Deno.test("Worker Console exposes a foldable timeline beside the scroll body", async () => { Deno.test("Worker Console exposes a foldable timeline beside the scroll body", async () => {
const consolePage = await Deno.readTextFile( const consolePage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
@ -114,7 +128,15 @@ Deno.test("Worker Console exposes a foldable timeline beside the scroll body", a
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("{#if timelineOpen}") && consolePage.includes("expanded={timelineOpen}") &&
!consolePage.includes("{#if timelineOpen}") &&
consolePage.includes("handleTimelineRailPointerDown") &&
consolePage.includes("projectTimelineAxisPosition") &&
consolePage.includes("scrollbar-width: none") &&
consoleTimeline.includes("onpointerdown={onRailPointerDown}") &&
consoleTimeline.includes("expanded ? 'expanded' : 'folded'") &&
consoleTimeline.includes(".timeline-mark.expanded .timeline-card") &&
!consoleTimeline.includes("{#if expanded}") &&
consoleTimeline.includes(".timeline-thumb") && consoleTimeline.includes(".timeline-thumb") &&
consoleTimeline.includes(".timeline-card"), consoleTimeline.includes(".timeline-card"),
"Worker Console should expose a foldable timeline with scroll and line jump markers", "Worker Console should expose a foldable timeline with scroll and line jump markers",
@ -124,23 +146,33 @@ Deno.test("Worker Console exposes a foldable timeline beside the scroll body", a
Deno.test("Worker Console composer fits to content without manual resize", async () => { Deno.test("Worker Console composer fits to content without manual resize", async () => {
const consolePage = await Deno.readTextFile( const consolePage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
assert( assert(
consolePage.includes("use:fitTextarea={{ value: draft, maxRows: 10 }}") && consolePage.includes("use:fitTextarea={{ value: draft, maxRows: 10 }}") &&
consolePage.includes("class=\"composer-input-shell\" onclick={handleComposerShellClick}") &&
consolePage.includes("bind:this={composerTextareaElement}") &&
consolePage.includes("composerTextareaElement?.focus()") &&
consolePage.includes('event.key === "PageUp" || event.key === "PageDown"') &&
consolePage.includes("scrollConsoleByPage") &&
consolePage.includes("class=\"composer-input-footer\"") &&
consolePage.includes("class=\"composer-footer-slot\"") &&
consolePage.includes("class=\"composer-send-button\"") &&
consolePage.includes("class=\"composer-send-icon\"") &&
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") &&
consolePage.includes("overflow-y: hidden"), consolePage.includes("overflow-y: hidden"),
"Console composer should autosize to content, cap at ten rows, and disable manual resize", "Console composer should autosize to content, cap at ten rows, wrap input and icon send button, and disable manual resize",
); );
}); });
Deno.test("Decodal source editor keeps imperative EditorView out of reactive state", async () => { Deno.test("Decodal source editor keeps imperative EditorView out of reactive state", async () => {
const editor = await Deno.readTextFile( const editor = await Deno.readTextFile(
new URL( new URL(
"../workspace-settings/DecodalSourceEditor.svelte", "../settings/DecodalSourceEditor.svelte",
import.meta.url, import.meta.url,
), ),
); );
@ -156,26 +188,26 @@ Deno.test("Decodal source editor keeps imperative EditorView out of reactive sta
Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned workdirs", async () => { Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned workdirs", async () => {
const sidebar = await Deno.readTextFile( const sidebar = await Deno.readTextFile(
new URL("../workspace-sidebar/WorkspaceSidebar.svelte", import.meta.url), new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
); );
const runtimesNav = await Deno.readTextFile( const runtimesNav = await Deno.readTextFile(
new URL("../workspace-sidebar/RuntimesNavSection.svelte", import.meta.url), new URL("../sidebar/RuntimesNavSection.svelte", import.meta.url),
); );
const runtimesPage = await Deno.readTextFile( const runtimesPage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
const workdirsPage = await Deno.readTextFile( const workdirsPage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
const workdirsLoad = await Deno.readTextFile( const workdirsLoad = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts",
import.meta.url, import.meta.url,
), ),
); );
@ -202,11 +234,11 @@ Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned
Deno.test("workspace Worker sidebar links New to the dedicated create page", async () => { Deno.test("workspace Worker sidebar links New to the dedicated create page", async () => {
const workersNav = await Deno.readTextFile( const workersNav = await Deno.readTextFile(
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url), new URL("../sidebar/WorkersNavSection.svelte", import.meta.url),
); );
const newWorkerPage = await Deno.readTextFile( const newWorkerPage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/workers/new/+page.svelte", "./../../../routes/w/[workspaceId]/workers/new/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
@ -228,13 +260,13 @@ Deno.test("workspace Worker sidebar links New to the dedicated create page", asy
Deno.test("Worker Console page is routed by runtime_id and worker_id through backend APIs", async () => { Deno.test("Worker Console page is routed by runtime_id and worker_id through backend APIs", async () => {
const consolePage = await Deno.readTextFile( const consolePage = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
import.meta.url, import.meta.url,
), ),
); );
const routeLoad = await Deno.readTextFile( const routeLoad = await Deno.readTextFile(
new URL( new URL(
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts", "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts",
import.meta.url, import.meta.url,
), ),
); );

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { RepositoryTicketsResponse, TicketSummary } from '$lib/workspace-sidebar/types'; import type { RepositoryTicketsResponse, TicketSummary } from '$lib/workspace/sidebar/types';
const INITIAL_VISIBLE_ROWS = 30; const INITIAL_VISIBLE_ROWS = 30;
const VISIBLE_ROW_INCREMENT = 30; const VISIBLE_ROW_INCREMENT = 30;

View File

@ -1,7 +1,7 @@
import { import {
workspaceApiJson, workspaceApiJson,
workspaceApiJsonWithBody, workspaceApiJsonWithBody,
} from "../workspace-api/http"; } from "../api/http";
import type { import type {
ProfileSettingsMutationResponse, ProfileSettingsMutationResponse,
ProfileSettingsResponse, ProfileSettingsResponse,

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { workspaceRoute } from '$lib/workspace-api/http'; import { workspaceRoute } from '$lib/workspace/api/http';
type Props = { type Props = {
currentPath?: string; currentPath?: string;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { workspaceApiPath } from '$lib/workspace-api/http'; import { workspaceApiPath } from '$lib/workspace/api/http';
import { workerConsoleHref } from '$lib/workspace-console/model'; import { workerConsoleHref } from '$lib/workspace/console/model';
import { canShowWorkerInSidebar } from './workers'; import { canShowWorkerInSidebar } from './workers';
import type { ListResponse, Worker } from './types'; import type { ListResponse, Worker } from './types';

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { workspaceRoute } from '$lib/workspace-api/http'; import { workspaceRoute } from '$lib/workspace/api/http';
import ObjectivesNavSection from './ObjectivesNavSection.svelte'; import ObjectivesNavSection from './ObjectivesNavSection.svelte';
import RepositoriesNavSection from './RepositoriesNavSection.svelte'; import RepositoriesNavSection from './RepositoriesNavSection.svelte';
import RuntimesNavSection from './RuntimesNavSection.svelte'; import RuntimesNavSection from './RuntimesNavSection.svelte';

View File

@ -1,4 +1,4 @@
import { workspaceRoute } from "$lib/workspace-api/http"; import { workspaceRoute } from "$lib/workspace/api/http";
import type { Diagnostic, RepositoryListResponse } from "./types"; import type { Diagnostic, RepositoryListResponse } from "./types";
export type RepositoryNavItem = { export type RepositoryNavItem = {

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import WorkspaceAlerts from '$lib/workspace-alerts/WorkspaceAlerts.svelte'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte'; import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte';
import '../app.css'; import '../app.css';
import type { LayoutProps } from './$types'; import type { LayoutProps } from './$types';

View File

@ -1,6 +1,6 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath, workspaceRoute } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath, workspaceRoute } 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 ssr = false; export const ssr = false;

View File

@ -1,5 +1,5 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { Host, ListResponse } from "$lib/workspace-sidebar/types"; import type { Host, ListResponse } 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 }) => {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { formatDate, workspaceRoute } from '$lib/workspace-api/http'; import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,5 +1,5 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { ObjectiveListResponse } from "$lib/workspace-sidebar/types"; import type { ObjectiveListResponse } 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 }) => {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { formatDate, workspaceRoute } from '$lib/workspace-api/http'; import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,8 +1,8 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { import type {
ObjectiveDetail, ObjectiveDetail,
ObjectiveListResponse, ObjectiveListResponse,
} from "$lib/workspace-sidebar/types"; } 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 }) => {

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { formatDate } from '$lib/workspace-api/http'; import { formatDate } from '$lib/workspace/api/http';
import RepositoryTicketKanban from '$lib/workspace-pages/RepositoryTicketKanban.svelte'; import RepositoryTicketKanban from '$lib/workspace/pages/RepositoryTicketKanban.svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,9 +1,9 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { import type {
RepositoryDetailResponse, RepositoryDetailResponse,
RepositoryLogResponse, RepositoryLogResponse,
RepositoryTicketsResponse, RepositoryTicketsResponse,
} from "$lib/workspace-sidebar/types"; } 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 }) => {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Runtime } from '$lib/workspace-sidebar/types'; import type { Runtime } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,5 +1,5 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { ListResponse, Runtime } from "$lib/workspace-sidebar/types"; import type { ListResponse, Runtime } 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 }) => {

View File

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { pushWorkspaceAlert } from '$lib/workspace-alerts/store'; import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import { workspaceApiPath } from '$lib/workspace-api/http'; import { workspaceApiPath } from '$lib/workspace/api/http';
import type { import type {
CleanupWorkdirCandidate, CleanupWorkdirCandidate,
RuntimeCleanupExecutionResponse, RuntimeCleanupExecutionResponse,
RuntimeCleanupPlanResponse, RuntimeCleanupPlanResponse,
WorkingDirectorySummary, WorkingDirectorySummary,
} from '$lib/workspace-sidebar/types'; } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,10 +1,10 @@
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { import type {
BrowserWorkingDirectoryListResponse, BrowserWorkingDirectoryListResponse,
ListResponse, ListResponse,
Runtime, Runtime,
RuntimeCleanupPlanResponse, RuntimeCleanupPlanResponse,
} from "$lib/workspace-sidebar/types"; } 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 }) => {

View File

@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { tick } from "svelte"; import { tick } from "svelte";
import ConsoleLineItem from "$lib/workspace-console/ConsoleLineItem.svelte"; import ConsoleLineItem from "$lib/workspace/console/ConsoleLineItem.svelte";
import ConsoleTimeline from "$lib/workspace-console/ConsoleTimeline.svelte"; import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte";
import { chatSubmit } from "$lib/workspace-console/chat-submit"; import { chatSubmit } from "$lib/workspace/console/chat-submit";
import { buildComposerRequest } from "$lib/workspace-console/composer-command"; import { buildComposerRequest } from "$lib/workspace/console/composer-command";
import { import {
applyCompletion, applyCompletion,
completionTokenAt, completionTokenAt,
localCommandCompletions, localCommandCompletions,
type ComposerCompletionEntry, type ComposerCompletionEntry,
type ComposerCompletionToken, type ComposerCompletionToken,
} from "$lib/workspace-console/composer-completion"; } from "$lib/workspace/console/composer-completion";
import { fitTextarea } from "$lib/workspace-console/textarea-fit"; import { fitTextarea } from "$lib/workspace/console/textarea-fit";
import { import {
createConsoleProjector, createConsoleProjector,
type ConsoleEventInput, type ConsoleEventInput,
type ConsoleLine, type ConsoleLine,
type ConsoleProjection, type ConsoleProjection,
} from "$lib/workspace-console/model"; } from "$lib/workspace/console/model";
import { workspaceApiPath } from "$lib/workspace-api/http"; import { workspaceApiPath } from "$lib/workspace/api/http";
import type { import type {
ClientWorkerEventWsFrame, ClientWorkerEventWsFrame,
Diagnostic, Diagnostic,
Worker, Worker,
WorkerInputResult, WorkerInputResult,
PodProtocolEvent, PodProtocolEvent,
} from "$lib/workspace-sidebar/types"; } from "$lib/workspace/sidebar/types";
type Props = { type Props = {
data: { data: {
@ -106,6 +106,8 @@
let workerDetailsOpen = $state(false); let workerDetailsOpen = $state(false);
let timelineOpen = $state(false); let timelineOpen = $state(false);
let consoleBodyElement: HTMLElement | null = null; let consoleBodyElement: HTMLElement | null = null;
let composerTextareaElement: HTMLTextAreaElement | null = null;
let timelineRailDragCleanup: (() => void) | null = null;
let autoFollowConsole = $state(true); let autoFollowConsole = $state(true);
let consoleScroll = $state<ScrollMetrics>({ top: 0, height: 1, client: 1 }); let consoleScroll = $state<ScrollMetrics>({ top: 0, height: 1, client: 1 });
const eventObservedAtById = new Map<string, number>(); const eventObservedAtById = new Map<string, number>();
@ -365,6 +367,11 @@
} }
function handleComposerKeydown(event: KeyboardEvent) { function handleComposerKeydown(event: KeyboardEvent) {
if (event.key === "PageUp" || event.key === "PageDown") {
event.preventDefault();
scrollConsoleByPage(event.key === "PageDown" ? 1 : -1);
return;
}
if (event.key !== "Tab") { if (event.key !== "Tab") {
return; return;
} }
@ -372,6 +379,28 @@
void applyComposerCompletion(event); void applyComposerCompletion(event);
} }
function scrollConsoleByPage(direction: 1 | -1) {
if (!consoleBodyElement) {
return;
}
consoleBodyElement.scrollBy({
top: direction * Math.max(consoleBodyElement.clientHeight * 0.86, 1),
behavior: "auto",
});
window.requestAnimationFrame(updateConsoleScrollMetrics);
}
function handleComposerShellClick(event: MouseEvent) {
const target = event.target;
if (
target instanceof Element &&
target.closest("button, textarea, a")
) {
return;
}
composerTextareaElement?.focus();
}
async function submitDraft(value = draft) { async function submitDraft(value = draft) {
const command = buildComposerRequest(value); const command = buildComposerRequest(value);
if (!command.ok) { if (!command.ok) {
@ -534,7 +563,7 @@
return { return {
scale, scale,
axisSize, axisSize,
marks: positioned, marks: projectTimelineMarks(positioned, axisSize, trackHeight),
}; };
} }
@ -576,6 +605,28 @@
return positioned; return positioned;
} }
function projectTimelineMarks(
marks: TimelineMark[],
axisSize: number,
trackHeight: number,
): TimelineMark[] {
return marks.map((mark) => ({
...mark,
position: projectTimelineAxisPosition(mark.position, axisSize, trackHeight),
}));
}
function projectTimelineAxisPosition(
axisPosition: number,
axisSize: number,
trackHeight: number,
): number {
if (axisSize <= 0) {
return 0;
}
return (axisPosition / axisSize) * trackHeight;
}
function timelineAxisSize( function timelineAxisSize(
marks: TimelineMark[], marks: TimelineMark[],
trackHeight: number, trackHeight: number,
@ -748,6 +799,7 @@
metrics: ScrollMetrics, metrics: ScrollMetrics,
layout: TimelineLayout, layout: TimelineLayout,
): string { ): string {
const trackHeight = timelineTrackHeight(metrics);
const contentHeight = Math.max(metrics.height, 1); const contentHeight = Math.max(metrics.height, 1);
const viewportHeight = Math.max(metrics.client, 1); const viewportHeight = Math.max(metrics.client, 1);
const scrollable = Math.max(contentHeight - viewportHeight, 1); const scrollable = Math.max(contentHeight - viewportHeight, 1);
@ -757,9 +809,19 @@
const sourceBottom = Math.min(100, sourceTop + viewportRatio * 100); const sourceBottom = Math.min(100, sourceTop + viewportRatio * 100);
const targetTop = mapTimelinePosition(layout.scale, sourceTop); const targetTop = mapTimelinePosition(layout.scale, sourceTop);
const targetBottom = mapTimelinePosition(layout.scale, sourceBottom); const targetBottom = mapTimelinePosition(layout.scale, sourceBottom);
const height = Math.max(targetBottom - targetTop, 18); const projectedTop = projectTimelineAxisPosition(
const top = Math.min(targetTop, Math.max(0, layout.axisSize - height)); targetTop,
return `top: ${Math.max(0, top)}px; height: ${Math.min(height, layout.axisSize)}px;`; layout.axisSize,
trackHeight,
);
const projectedBottom = projectTimelineAxisPosition(
targetBottom,
layout.axisSize,
trackHeight,
);
const height = Math.max(projectedBottom - projectedTop, 18);
const top = Math.min(projectedTop, Math.max(0, trackHeight - height));
return `top: ${Math.max(0, top)}px; height: ${Math.min(height, trackHeight)}px;`;
} }
function timelineTrackHeight(metrics: ScrollMetrics): number { function timelineTrackHeight(metrics: ScrollMetrics): number {
@ -767,16 +829,12 @@
} }
function timelineAxisStyleFor( function timelineAxisStyleFor(
layout: TimelineLayout, _layout: TimelineLayout,
metrics: ScrollMetrics, metrics: ScrollMetrics,
): string { ): string {
const trackHeight = timelineTrackHeight(metrics);
const scrollable = Math.max(metrics.height - metrics.client, 1);
const scrollRatio = Math.max(0, Math.min(1, metrics.top / scrollable));
const offset = Math.max(0, layout.axisSize - trackHeight) * scrollRatio;
return [ return [
`height: ${layout.axisSize}px`, `height: ${timelineTrackHeight(metrics)}px`,
`top: ${TIMELINE_AXIS_PADDING_PX - offset}px`, `top: ${TIMELINE_AXIS_PADDING_PX}px`,
].join("; "); ].join("; ");
} }
@ -789,21 +847,21 @@
} }
} }
function jumpTimelineRatio(event: MouseEvent) { function scrollConsoleToTimelineRailPosition(
if ( rail: HTMLElement,
!consoleBodyElement || clientY: number,
!(event.currentTarget instanceof HTMLElement) behavior: ScrollBehavior,
) { ) {
if (!consoleBodyElement) {
return; return;
} }
const rect = event.currentTarget.getBoundingClientRect(); const rect = rail.getBoundingClientRect();
const targetPx = Math.max( const trackHeight = Math.max(rect.height, 1);
0, const targetPx = Math.max(0, Math.min(trackHeight, clientY - rect.top));
Math.min(rect.height, event.clientY - rect.top), const axisPosition = (targetPx / trackHeight) * timelineLayout.axisSize;
);
const sourcePercent = unmapTimelinePosition( const sourcePercent = unmapTimelinePosition(
timelineLayout.scale, timelineLayout.scale,
targetPx, axisPosition,
); );
consoleBodyElement.scrollTo({ consoleBodyElement.scrollTo({
top: top:
@ -813,10 +871,50 @@
consoleBodyElement.clientHeight, consoleBodyElement.clientHeight,
0, 0,
), ),
behavior: "smooth", behavior,
}); });
} }
function handleTimelineRailPointerDown(event: PointerEvent) {
if (!(event.currentTarget instanceof HTMLElement)) {
return;
}
event.preventDefault();
const rail = event.currentTarget;
const pointerId = event.pointerId;
scrollConsoleToTimelineRailPosition(rail, event.clientY, "auto");
const handleMove = (moveEvent: PointerEvent) => {
if (moveEvent.pointerId !== pointerId) {
return;
}
moveEvent.preventDefault();
scrollConsoleToTimelineRailPosition(rail, moveEvent.clientY, "auto");
};
const stopDrag = (finishEvent: PointerEvent) => {
if (finishEvent.pointerId !== pointerId) {
return;
}
timelineRailDragCleanup?.();
};
timelineRailDragCleanup?.();
timelineRailDragCleanup = () => {
window.removeEventListener("pointermove", handleMove);
window.removeEventListener("pointerup", stopDrag);
window.removeEventListener("pointercancel", stopDrag);
timelineRailDragCleanup = null;
};
window.addEventListener("pointermove", handleMove);
window.addEventListener("pointerup", stopDrag);
window.addEventListener("pointercancel", stopDrag);
try {
rail.setPointerCapture(pointerId);
} catch {
// Pointer capture can fail if the pointer is already released.
}
}
function cssEscape(value: string): string { function cssEscape(value: string): string {
return typeof CSS !== "undefined" && typeof CSS.escape === "function" return typeof CSS !== "undefined" && typeof CSS.escape === "function"
? CSS.escape(value) ? CSS.escape(value)
@ -877,6 +975,10 @@
} }
}); });
$effect(() => {
return () => timelineRailDragCleanup?.();
});
$effect(() => { $effect(() => {
const target = consoleTarget; const target = consoleTarget;
resetObservedEvents(); resetObservedEvents();
@ -955,15 +1057,14 @@
</article> </article>
</div> </div>
{#if timelineOpen}
<ConsoleTimeline <ConsoleTimeline
marks={timelineMarks} marks={timelineMarks}
thumbStyle={timelineThumb} thumbStyle={timelineThumb}
axisStyle={timelineAxisStyle} axisStyle={timelineAxisStyle}
onRailClick={jumpTimelineRatio} expanded={timelineOpen}
onRailPointerDown={handleTimelineRailPointerDown}
onMarkClick={jumpToTimelineMark} onMarkClick={jumpToTimelineMark}
/> />
{/if}
</section> </section>
{#if workerDetailsOpen} {#if workerDetailsOpen}
@ -1053,10 +1154,12 @@
{/if} {/if}
<form class="console-composer card" onsubmit={sendMessage}> <form class="console-composer card" onsubmit={sendMessage}>
<div class="composer-input-shell" onclick={handleComposerShellClick}>
<textarea <textarea
id="worker-console-message" id="worker-console-message"
aria-label="Console input" aria-label="Console input"
aria-keyshortcuts="Meta+Enter Control+Enter" aria-keyshortcuts="Meta+Enter Control+Enter"
bind:this={composerTextareaElement}
bind:value={draft} bind:value={draft}
use:chatSubmit={{ use:chatSubmit={{
enabled: inputReady && !sending, enabled: inputReady && !sending,
@ -1065,6 +1168,8 @@
use:fitTextarea={{ value: draft, maxRows: 10 }} use:fitTextarea={{ value: draft, maxRows: 10 }}
onkeydown={handleComposerKeydown} onkeydown={handleComposerKeydown}
disabled={!inputReady || sending}></textarea> disabled={!inputReady || sending}></textarea>
<div class="composer-input-footer">
<div class="composer-footer-slot">
{#if completionBusy || completionError || completionEntries.length > 0} {#if completionBusy || completionError || completionEntries.length > 0}
<div class="composer-completions" aria-live="polite"> <div class="composer-completions" aria-live="polite">
{#if completionBusy} {#if completionBusy}
@ -1082,10 +1187,25 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<div class="composer-actions"> </div>
<button type="submit" disabled={!canSend}> <button
{sending ? "Sending…" : "Send"} class="composer-send-button"
type="submit"
aria-label={sending ? "Sending message" : "Send message"}
disabled={!canSend}
>
<svg
class="composer-send-icon"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path d="M8 6L12 2L16 6" />
<path d="M12 2V22" />
</svg>
</button> </button>
</div>
</div>
<div class="composer-actions">
{#if composerNotice} {#if composerNotice}
<span class="composer-notice">{composerNotice}</span> <span class="composer-notice">{composerNotice}</span>
{/if} {/if}
@ -1203,32 +1323,13 @@
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
padding-right: var(--space-2); padding-right: 0;
scrollbar-color: color-mix(in srgb, var(--tui-cyan) 60%, var(--line)) scrollbar-gutter: auto;
color-mix(in srgb, var(--bg-raised) 70%, transparent); scrollbar-width: none;
scrollbar-gutter: stable;
scrollbar-width: thin;
} }
.console-scroll::-webkit-scrollbar { .console-scroll::-webkit-scrollbar {
width: 0.65rem; display: none;
}
.console-scroll::-webkit-scrollbar-track {
border-radius: 999px;
background: color-mix(in srgb, var(--bg-raised) 70%, transparent);
}
.console-scroll::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 999px;
background: color-mix(in srgb, var(--tui-cyan) 60%, var(--line));
background-clip: padding-box;
}
.console-scroll::-webkit-scrollbar-thumb:hover {
background: var(--tui-cyan);
background-clip: padding-box;
} }
.console-log { .console-log {
@ -1309,25 +1410,82 @@
background: var(--bg); background: var(--bg);
} }
.composer-input-shell {
display: grid;
gap: var(--space-2);
border: 1px solid var(--line);
border-radius: 18px;
background: var(--bg-raised);
cursor: text;
padding: 0.35rem;
}
.composer-input-shell:focus-within {
border-color: color-mix(in srgb, var(--tui-cyan) 60%, var(--line));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--tui-cyan) 18%, transparent);
}
.composer-input-footer {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-2);
align-items: end;
min-height: 2.35rem;
padding-left: 0.65rem;
}
.composer-footer-slot {
min-width: 0;
}
.console-composer textarea { .console-composer textarea {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
min-height: 0; min-height: 0;
resize: none; resize: none;
overflow-y: hidden; overflow-y: hidden;
border: 1px solid var(--line); border: 0;
border-radius: 14px; border-radius: 14px;
padding: 0.85rem 1rem; background: transparent;
padding: 0.55rem 0.65rem;
font: inherit; font: inherit;
line-height: 1.45; line-height: 1.45;
color: var(--text-strong); color: var(--text-strong);
outline: none;
} }
.console-composer textarea:disabled { .console-composer textarea:disabled {
background: var(--bg-raised);
color: var(--text-muted); color: var(--text-muted);
} }
.composer-send-button {
display: inline-grid;
width: 2.35rem;
height: 2.35rem;
place-items: center;
border: 0;
border-radius: 999px;
background: var(--accent);
color: var(--bg);
cursor: pointer;
padding: 0;
}
.composer-send-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.composer-send-icon {
width: 1.2rem;
height: 1.2rem;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.composer-completions { .composer-completions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1354,21 +1512,6 @@
margin-right: auto; margin-right: auto;
} }
.composer-actions button {
border: 0;
border-radius: 999px;
padding: 0.65rem 1rem;
background: var(--accent);
color: var(--bg);
font-weight: 800;
cursor: pointer;
}
.composer-actions button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
@media (max-width: 960px) { @media (max-width: 960px) {
.console-header { .console-header {
flex-direction: column; flex-direction: column;

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { workspaceRoute } from '$lib/workspace-api/http'; import { workspaceRoute } from '$lib/workspace/api/http';
import { SETTINGS_SECTIONS, settingsSectionHref } from '$lib/workspace-settings/model'; import { SETTINGS_SECTIONS, settingsSectionHref } from '$lib/workspace/settings/model';
import type { LayoutProps } from './$types'; import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props(); let { data, children }: LayoutProps = $props();

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { workspaceRoute } from '$lib/workspace-api/http'; import { workspaceRoute } from '$lib/workspace/api/http';
import { SETTINGS_SECTIONS, settingsSectionHref } from '$lib/workspace-settings/model'; import { SETTINGS_SECTIONS, settingsSectionHref } from '$lib/workspace/settings/model';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import DiagnosticsList from '$lib/workspace-settings/DiagnosticsList.svelte'; import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import DecodalSourceEditor from '$lib/workspace-settings/DecodalSourceEditor.svelte'; import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
import { import {
fetchProfileSettings, fetchProfileSettings,
fetchProfileSourceTree, fetchProfileSourceTree,
deleteProfileTreeFile, deleteProfileTreeFile,
fetchProfileTreeFile, fetchProfileTreeFile,
writeProfileTreeFile, writeProfileTreeFile,
} from '$lib/workspace-settings/profile-api'; } from '$lib/workspace/settings/profile-api';
import type { Diagnostic } from '$lib/workspace-settings/model'; import type { Diagnostic } from '$lib/workspace/settings/model';
import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from '$lib/workspace-settings/profile-routes'; import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
import type { import type {
ProfileSettingsResponse, ProfileSettingsResponse,
WorkspaceProfileSourceTreeFileResponse, WorkspaceProfileSourceTreeFileResponse,
WorkspaceProfileSourceTreeResponse, WorkspaceProfileSourceTreeResponse,
} from '$lib/workspace-settings/profile-types'; } from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import DiagnosticsList from '$lib/workspace-settings/DiagnosticsList.svelte'; import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import DecodalSourceEditor from '$lib/workspace-settings/DecodalSourceEditor.svelte'; import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
import { import {
deleteProfileTreeFile, deleteProfileTreeFile,
fetchProfileSourceTree, fetchProfileSourceTree,
fetchProfileTreeFile, fetchProfileTreeFile,
writeProfileTreeFile, writeProfileTreeFile,
} from '$lib/workspace-settings/profile-api'; } from '$lib/workspace/settings/profile-api';
import { profileSettingsHref, virtualProfilePathForCreate } from '$lib/workspace-settings/profile-routes'; import { profileSettingsHref, virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
import type { Diagnostic } from '$lib/workspace-settings/model'; import type { Diagnostic } from '$lib/workspace/settings/model';
import type { import type {
WorkspaceProfileSourceTreeFileResponse, WorkspaceProfileSourceTreeFileResponse,
WorkspaceProfileSourceTreeResponse, WorkspaceProfileSourceTreeResponse,
} from '$lib/workspace-settings/profile-types'; } from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { workspaceApiPath } from '$lib/workspace-api/http'; import { workspaceApiPath } from '$lib/workspace/api/http';
import DiagnosticsList from '$lib/workspace-settings/DiagnosticsList.svelte'; import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import type { import type {
Diagnostic, Diagnostic,
RemoteRuntimeTestResponse, RemoteRuntimeTestResponse,
RuntimeConnectionMutationResponse, RuntimeConnectionMutationResponse,
RuntimeConnectionSettingsResponse RuntimeConnectionSettingsResponse
} from '$lib/workspace-settings/model'; } from '$lib/workspace/settings/model';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
type RemoteAddForm = { type RemoteAddForm = {

View File

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import DiagnosticsList from '$lib/workspace-settings/DiagnosticsList.svelte'; import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import { import {
fetchWorkspaceMetadataSettings, fetchWorkspaceMetadataSettings,
updateWorkspaceMetadataSettings updateWorkspaceMetadataSettings
} from '$lib/workspace-settings/profile-api'; } from '$lib/workspace/settings/profile-api';
import type { Diagnostic } from '$lib/workspace-settings/model'; import type { Diagnostic } from '$lib/workspace/settings/model';
import type { WorkspaceMetadataSettingsResponse } from '$lib/workspace-settings/profile-types'; import type { WorkspaceMetadataSettingsResponse } from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();

View File

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { pushWorkspaceAlert } from '$lib/workspace-alerts/store'; import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import { workspaceApiPath } from '$lib/workspace-api/http'; import { workspaceApiPath } from '$lib/workspace/api/http';
import { workerConsoleHref } from '$lib/workspace-console/model'; import { workerConsoleHref } from '$lib/workspace/console/model';
import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers'; import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace-sidebar/types'; import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
type WorkerActionKind = 'pin' | 'delete'; type WorkerActionKind = 'pin' | 'delete';

View File

@ -1,5 +1,5 @@
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 }) => {

View File

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { workspaceApiPath } from '$lib/workspace-api/http'; import { workspaceApiPath } from '$lib/workspace/api/http';
import { buildBrowserCreateWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace-sidebar/worker-launch'; import { buildBrowserCreateWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
import type { import type {
BrowserCreateWorkerResponse, BrowserCreateWorkerResponse,
BrowserWorkingDirectoryCreateResponse, BrowserWorkingDirectoryCreateResponse,
Diagnostic, Diagnostic,
WorkerLaunchOptionsResponse, WorkerLaunchOptionsResponse,
} from '$lib/workspace-sidebar/types'; } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
type DisplayError = { type DisplayError = {