summaryrefslogtreecommitdiffhomepage
path: root/src/app/App.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 20:49:43 +0900
committerAdam Malczewski <[email protected]>2026-06-27 20:49:43 +0900
commita59200e786f7d97d7ba5b9cd2bee9ffef531dac2 (patch)
tree92088287487e34a9fee37f89325961110b1d3442 /src/app/App.svelte
parenta9ca756de8cd023c0f2cb9954f344fff11146bc2 (diff)
parentb70ae547fdcb8c1794981957485537dc21a8b5fd (diff)
downloaddispatch-web-a59200e786f7d97d7ba5b9cd2bee9ffef531dac2.tar.gz
dispatch-web-a59200e786f7d97d7ba5b9cd2bee9ffef531dac2.zip
Merge branch 'feature/vision-handoff' into dev
# Conflicts: # .dispatch/transport-contract.reference.md # backend-handoff.md # src/app/App.svelte # src/features/chat/ui/Composer.svelte
Diffstat (limited to 'src/app/App.svelte')
-rw-r--r--src/app/App.svelte55
1 files changed, 50 insertions, 5 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte
index b46885c..59f949c 100644
--- a/src/app/App.svelte
+++ b/src/app/App.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import type { ReasoningEffort } from "@dispatch/transport-contract";
+ import type { ImageInput, ReasoningEffort } from "@dispatch/transport-contract";
import type { InvokeMessage } from "@dispatch/ui-contract";
import { tick } from "svelte";
import Table from "../components/Table.svelte";
@@ -86,6 +86,13 @@
type SaveSystemPrompt as SaveSystemPromptAlias,
manifest as systemPromptManifest,
} from "../features/system-prompt";
+ import {
+ VisionSettingsView,
+ manifest as visionManifest,
+ type LoadVisionSettingsResult,
+ type SaveVisionSettingsResult,
+ type VisionSettingsPatch,
+ } from "../features/vision";
import type { AppStore } from "./store.svelte";
import ErrorModal from "./ErrorModal.svelte";
import { createLocalStore } from "../adapters/local-storage";
@@ -156,6 +163,7 @@
systemPromptManifest,
heartbeatManifest,
concurrencyManifest,
+ visionManifest,
].map((m) => [m.name, m.description] as const);
// Smart-scroll: keep the transcript pinned to the bottom while it streams,
@@ -279,8 +287,8 @@
store.invoke(msg.surfaceId, msg.actionId, msg.payload);
}
- function handleSend(text: string) {
- store.send(text);
+ function handleSend(text: string, images?: readonly ImageInput[]): void {
+ store.send(text, images);
}
function handleQueue(text: string) {
@@ -342,6 +350,27 @@
: { ok: false, error: result.error };
}
+ // Adapt the store's global vision-settings API to the vision feature's ports.
+ async function loadVisionSettings(): Promise<LoadVisionSettingsResult> {
+ // The store seeds `visionSettings` on boot; a refresh keeps it current.
+ await store.refreshVisionSettings();
+ const settings = store.visionSettings;
+ if (settings === null) {
+ return { ok: false, error: "Vision settings not available." };
+ }
+ return { ok: true, settings };
+ }
+
+ async function saveVisionSettings(
+ patch: VisionSettingsPatch,
+ ): Promise<SaveVisionSettingsResult> {
+ const result = await store.setVisionSettings(patch);
+ if (result === null) return { ok: false, error: "Vision settings not available." };
+ return result.ok
+ ? { ok: true, settings: result.settings }
+ : { ok: false, error: result.error };
+ }
+
// Adapt the store's chat-limit result to the settings feature's port. On a
// raise the active chat refills (prepends older history); preserve the
// reader's viewport over the prepend (the manual analogue of CSS scroll
@@ -572,6 +601,7 @@
onShowEarlier={handleShowEarlier}
thinkingKeyBase={store.activeChat.thinkingKeyBase}
providerRetry={store.activeChat.providerRetry}
+ apiBaseUrl={store.httpBase}
/>
{/key}
</div>
@@ -662,6 +692,7 @@
closeChat={closeRunChat}
stopRun={stopHeartbeatRun}
onClose={() => (heartbeatRun = null)}
+ apiBaseUrl={store.httpBase}
/>
{/key}
{/if}
@@ -684,7 +715,12 @@
{/key}
{:else if kind === "model"}
<div class="flex flex-col gap-3">
- <ModelSelector models={store.models} selected={store.activeModel} onSelect={handleSelectModel} />
+ <ModelSelector
+ models={store.models}
+ selected={store.activeModel}
+ onSelect={handleSelectModel}
+ modelInfo={store.modelInfo}
+ />
<!-- Keyed on the workspace conversation (active tab OR draft) so the inputs
re-mount per conversation — incl. switching between drafts — and can't
bleed across tabs. Editable for a draft too (cwd + effort apply from turn 1). -->
@@ -743,7 +779,9 @@
{/if}
{/key}
{:else if kind === "compaction"}
- <!-- Re-mount per conversation so the percent + feedback can't bleed across tabs. -->
+ <!-- Message compaction is per-conversation (keyed so the percent + feedback
+ can't bleed across tabs). Vision (image-compaction) settings are GLOBAL,
+ so they stay mounted across conversation switches (no {#key}). -->
{#key store.currentConversationId}
<CompactionView
percent={store.compactPercent}
@@ -752,6 +790,13 @@
savePercent={saveCompactPercent}
/>
{/key}
+ <div class="divider my-1 text-xs text-base-content/40">Image compaction</div>
+ <VisionSettingsView
+ models={store.models}
+ modelInfo={store.modelInfo}
+ load={loadVisionSettings}
+ save={saveVisionSettings}
+ />
{:else if kind === "system-prompt"}
<!-- Global system prompt template. Opens a full-page modal editor (half
template / half variable palette). Not conversation-scoped (no {#key}). -->