Skip to content

Commit 0fe740d

Browse files
committed
fix(ui): preserve hidden session state
1 parent 63629ca commit 0fe740d

9 files changed

Lines changed: 130 additions & 86 deletions

File tree

packages/ui/src/components/instance/instance-shell2.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ function RightPanelFallback() {
6969
return <div class="flex-1 min-h-0" />
7070
}
7171

72+
function CommandPaletteFallback(props: { title: string; loadingLabel: string }) {
73+
return (
74+
<div class="fixed inset-0 z-[70] flex items-start justify-center bg-black/40 px-4 pt-[12vh]">
75+
<div class="w-full max-w-xl rounded-xl border bg-[var(--surface-base)] px-5 py-4 shadow-xl">
76+
<div class="text-sm font-semibold text-[var(--text-primary)]">{props.title}</div>
77+
<div class="mt-2 text-sm text-[var(--text-secondary)]">{props.loadingLabel}</div>
78+
</div>
79+
</div>
80+
)
81+
}
82+
7283
interface InstanceShellProps {
7384
instance: Instance
7485
// Provided by App-level instance tabs; lets us pause heavy rendering
@@ -884,7 +895,14 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
884895
</div>
885896

886897
<Show when={paletteOpen()}>
887-
<Suspense fallback={null}>
898+
<Suspense
899+
fallback={
900+
<CommandPaletteFallback
901+
title={t("commandPalette.title")}
902+
loadingLabel={t("unifiedPicker.loading.loadingWorkspace")}
903+
/>
904+
}
905+
>
888906
<LazyCommandPalette
889907
open={paletteOpen()}
890908
onClose={() => hideCommandPalette(props.instance.id)}

packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
2-
3-
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
1+
import { For, Show, Suspense, createMemo, lazy, type Accessor, type Component, type JSX } from "solid-js"
42

53
import DiffToolbar from "../components/DiffToolbar"
64
import SplitFilePanel from "../components/SplitFilePanel"
75
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
86

7+
const LazyMonacoDiffViewer = lazy(() =>
8+
import("../../../../file-viewer/monaco-diff-viewer").then((module) => ({ default: module.MonacoDiffViewer })),
9+
)
10+
911
interface ChangesTabProps {
1012
t: (key: string, vars?: Record<string, any>) => string
1113

@@ -113,15 +115,17 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
113115
}
114116
>
115117
{(file) => (
116-
<MonacoDiffViewer
117-
scopeKey={scopeKey()}
118-
path={String(file().file || "")}
119-
before={String((file() as any).before || "")}
120-
after={String((file() as any).after || "")}
121-
viewMode={props.diffViewMode()}
122-
contextMode={props.diffContextMode()}
123-
wordWrap={props.diffWordWrapMode()}
124-
/>
118+
<Suspense fallback={<div class="file-viewer-empty"><span class="file-viewer-empty-text">{props.t("instanceShell.sessionChanges.loading")}</span></div>}>
119+
<LazyMonacoDiffViewer
120+
scopeKey={scopeKey()}
121+
path={String(file().file || "")}
122+
before={String((file() as any).before || "")}
123+
after={String((file() as any).after || "")}
124+
viewMode={props.diffViewMode()}
125+
contextMode={props.diffContextMode()}
126+
wordWrap={props.diffWordWrapMode()}
127+
/>
128+
</Suspense>
125129
)}
126130
</Show>
127131
</div>

packages/ui/src/components/instance/shell/right-panel/tabs/FilesTab.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { For, Show, type Accessor, type Component, type JSX } from "solid-js"
1+
import { For, Show, Suspense, lazy, type Accessor, type Component, type JSX } from "solid-js"
22
import type { FileNode } from "@opencode-ai/sdk/v2/client"
33

44
import { RefreshCw } from "lucide-solid"
55

6-
import { MonacoFileViewer } from "../../../../file-viewer/monaco-file-viewer"
7-
86
import SplitFilePanel from "../components/SplitFilePanel"
97

8+
const LazyMonacoFileViewer = lazy(() =>
9+
import("../../../../file-viewer/monaco-file-viewer").then((module) => ({ default: module.MonacoFileViewer })),
10+
)
11+
1012
interface FilesTabProps {
1113
t: (key: string, vars?: Record<string, any>) => string
1214

@@ -51,7 +53,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
5153
const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath()
5254

5355
const emptyViewerMessage = () => {
54-
if (props.browserLoading() && entriesValue === null) return "Loading files..."
56+
if (props.browserLoading() && entriesValue === null) return props.t("instanceInfo.loading")
5557
return "Select a file to preview"
5658
}
5759

@@ -77,7 +79,9 @@ const FilesTab: Component<FilesTabProps> = (props) => {
7779
}
7880
>
7981
{(payload) => (
80-
<MonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
82+
<Suspense fallback={<div class="file-viewer-empty"><span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span></div>}>
83+
<LazyMonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
84+
</Suspense>
8185
)}
8286
</Show>
8387
}
@@ -113,7 +117,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
113117
</Show>
114118

115119
<Show when={props.browserLoading() && entriesValue === null}>
116-
<div class="p-3 text-xs text-secondary">Loading files...</div>
120+
<div class="p-3 text-xs text-secondary">{props.t("instanceInfo.loading")}</div>
117121
</Show>
118122

119123
<For each={sorted}>

packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
1+
import { For, Show, Suspense, createMemo, lazy, type Accessor, type Component, type JSX } from "solid-js"
22
import type { File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
33

44
import { RefreshCw } from "lucide-solid"
55

6-
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
7-
86
import DiffToolbar from "../components/DiffToolbar"
97
import SplitFilePanel from "../components/SplitFilePanel"
108
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
119

10+
const LazyMonacoDiffViewer = lazy(() =>
11+
import("../../../../file-viewer/monaco-diff-viewer").then((module) => ({ default: module.MonacoDiffViewer })),
12+
)
13+
1214
interface GitChangesTabProps {
1315
t: (key: string, vars?: Record<string, any>) => string
1416

@@ -122,7 +124,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
122124
}
123125
>
124126
{(file) => (
125-
<MonacoDiffViewer
127+
<Suspense fallback={<div class="file-viewer-empty"><span class="file-viewer-empty-text">{props.t("instanceShell.sessionChanges.loading")}</span></div>}>
128+
<LazyMonacoDiffViewer
126129
scopeKey={props.scopeKey()}
127130
path={String(file().path || "")}
128131
before={String((file() as any).before || "")}
@@ -131,7 +134,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
131134
contextMode={props.diffContextMode()}
132135
wordWrap={props.diffWordWrapMode()}
133136
/>
134-
)}
137+
</Suspense>
138+
)}
135139
</Show>
136140
}
137141
>

packages/ui/src/components/prompt-input.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { createSignal, Show, onMount, onCleanup, createEffect, on } from "solid-js"
1+
import { Suspense, createEffect, createSignal, lazy, on, onCleanup, onMount, Show } from "solid-js"
22
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
3-
import UnifiedPicker from "./unified-picker"
43
import ExpandButton from "./expand-button"
54
import { clearAttachments, removeAttachment } from "../stores/attachments"
65
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
@@ -18,6 +17,7 @@ import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
1817
import { usePromptPicker } from "./prompt-input/usePromptPicker"
1918
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
2019
const log = getLogger("actions")
20+
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
2121

2222
export default function PromptInput(props: PromptInputProps) {
2323
const { t } = useI18n()
@@ -428,18 +428,26 @@ export default function PromptInput(props: PromptInputProps) {
428428
onDrop={handleDrop}
429429
>
430430
<Show when={showPicker() && instance()}>
431-
<UnifiedPicker
432-
open={showPicker()}
433-
mode={pickerMode()}
434-
onClose={handlePickerClose}
435-
onSelect={handlePickerSelect}
436-
agents={instanceAgents()}
437-
commands={getCommands(props.instanceId)}
438-
instanceClient={instance()!.client}
439-
searchQuery={searchQuery()}
440-
textareaRef={textareaRef}
441-
workspaceId={props.instanceId}
442-
/>
431+
<Suspense
432+
fallback={
433+
<div class="unified-picker unified-picker-loading" role="status" aria-live="polite">
434+
{t("unifiedPicker.loading.loadingWorkspace")}
435+
</div>
436+
}
437+
>
438+
<LazyUnifiedPicker
439+
open={showPicker()}
440+
mode={pickerMode()}
441+
onClose={handlePickerClose}
442+
onSelect={handlePickerSelect}
443+
agents={instanceAgents()}
444+
commands={getCommands(props.instanceId)}
445+
instanceClient={instance()!.client}
446+
searchQuery={searchQuery()}
447+
textareaRef={textareaRef}
448+
workspaceId={props.instanceId}
449+
/>
450+
</Suspense>
443451
</Show>
444452

445453
<div class="flex flex-1 flex-col">

packages/ui/src/components/session/session-view.tsx

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
328328
if (!activeSession) return null
329329
return (
330330
<div ref={rootRef} class="session-view">
331-
<Show when={isActiveSession()} fallback={<div class="flex-1 min-h-0" />}>
331+
<div class="flex-1 min-h-0" style={{ display: isActiveSession() ? "flex" : "none" }}>
332332
<MessageSection
333333
instanceId={props.instanceId}
334334
sessionId={activeSession.id}
@@ -350,44 +350,41 @@ export const SessionView: Component<SessionViewProps> = (props) => {
350350
forceCompactStatusLayout={props.forceCompactStatusLayout}
351351
onQuoteSelection={handleQuoteSelection}
352352
/>
353-
</Show>
354-
355-
356-
<Show when={isActiveSession()}>
357-
<>
358-
<Show when={attachments().length > 0}>
359-
<PromptAttachmentsBar
360-
attachments={attachments()}
361-
onRemoveAttachment={(attachmentId) => {
362-
if (promptInputApi) {
363-
promptInputApi.removeAttachment(attachmentId)
364-
return
365-
}
366-
removeAttachment(props.instanceId, props.sessionId, attachmentId)
367-
}}
368-
onExpandTextAttachment={(attachmentId) => promptInputApi?.expandTextAttachment(attachmentId)}
369-
/>
370-
</Show>
371-
372-
<PromptInput
373-
instanceId={props.instanceId}
374-
instanceFolder={props.instanceFolder}
375-
sessionId={activeSession.id}
376-
isActive={props.isActive}
377-
compactLayout={props.compactPromptLayout}
378-
onSend={handleSendMessage}
379-
onRunShell={handleRunShell}
380-
escapeInDebounce={props.escapeInDebounce}
381-
isSessionBusy={sessionBusy()}
382-
disabled={sessionNeedsInput()}
383-
onAbortSession={handleAbortSession}
384-
registerPromptInputApi={registerPromptInputApi}
385-
/>
386-
</>
353+
</div>
354+
355+
<div style={{ display: isActiveSession() ? "block" : "none" }}>
356+
<Show when={attachments().length > 0}>
357+
<PromptAttachmentsBar
358+
attachments={attachments()}
359+
onRemoveAttachment={(attachmentId) => {
360+
if (promptInputApi) {
361+
promptInputApi.removeAttachment(attachmentId)
362+
return
363+
}
364+
removeAttachment(props.instanceId, props.sessionId, attachmentId)
365+
}}
366+
onExpandTextAttachment={(attachmentId) => promptInputApi?.expandTextAttachment(attachmentId)}
367+
/>
387368
</Show>
369+
370+
<PromptInput
371+
instanceId={props.instanceId}
372+
instanceFolder={props.instanceFolder}
373+
sessionId={activeSession.id}
374+
isActive={props.isActive}
375+
compactLayout={props.compactPromptLayout}
376+
onSend={handleSendMessage}
377+
onRunShell={handleRunShell}
378+
escapeInDebounce={props.escapeInDebounce}
379+
isSessionBusy={sessionBusy()}
380+
disabled={sessionNeedsInput()}
381+
onAbortSession={handleAbortSession}
382+
registerPromptInputApi={registerPromptInputApi}
383+
/>
388384
</div>
389-
)
390-
}}
385+
</div>
386+
)
387+
}}
391388
</Show>
392389
)
393390
}

packages/ui/src/lib/ui-bootstrap-cache.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export type UiBootstrapTheme = "light" | "dark" | "system"
22

33
export interface UiBootstrapCacheSnapshot {
4-
theme?: UiBootstrapTheme | null
5-
locale?: string | null
4+
theme?: UiBootstrapTheme
5+
locale?: string
66
}
77

88
const UI_BOOTSTRAP_CACHE_KEY = "codenomad:ui-bootstrap"
@@ -43,12 +43,11 @@ export function writeUiBootstrapCache(snapshot: UiBootstrapCacheSnapshot) {
4343
try {
4444
const previous = readUiBootstrapCache()
4545
const next: UiBootstrapCacheSnapshot = {
46-
theme: snapshot.theme === undefined ? previous.theme : snapshot.theme ?? undefined,
47-
locale: snapshot.locale === undefined ? previous.locale : snapshot.locale ?? undefined,
46+
theme: snapshot.theme ?? previous.theme,
47+
locale: snapshot.locale ?? previous.locale,
4848
}
4949

5050
if (!next.theme && !next.locale) {
51-
window.localStorage.removeItem(UI_BOOTSTRAP_CACHE_KEY)
5251
return
5352
}
5453

packages/ui/src/stores/preferences.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -600,17 +600,17 @@ const configContextValue: ConfigContextValue = {
600600

601601
export const ConfigProvider: ParentComponent = (props) => {
602602
createEffect(() => {
603-
if (!isLoaded()) {
604-
return
605-
}
606-
607603
const bucket = uiConfigBucket()
608604
const theme = bucket.theme
609605
const locale = bucket.settings?.locale
610606

607+
if (!theme && !locale) {
608+
return
609+
}
610+
611611
writeUiBootstrapCache({
612-
theme: theme ?? null,
613-
locale: locale ?? null,
612+
theme,
613+
locale,
614614
})
615615
})
616616

packages/ui/src/styles/messaging/tool-call.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@
7676
--tool-call-max-height-large: calc(var(--tool-call-lines-large) * var(--tool-call-line-unit));
7777
}
7878

79+
.tool-call-loading {
80+
min-height: 3rem;
81+
border-left: 3px solid var(--border-muted, var(--border-base));
82+
background:
83+
linear-gradient(90deg, transparent, color-mix(in srgb, var(--surface-hover) 72%, transparent), transparent)
84+
var(--surface-secondary);
85+
background-size: 200% 100%;
86+
animation: shimmer 1.6s ease-in-out infinite;
87+
}
88+
7989
.tool-call-message .tool-call {
8090
border: none;
8191
border-radius: 0;

0 commit comments

Comments
 (0)