Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
import { t } from "../../../i18n";
import { FileText, RefreshCcw, Save, X } from "lucide-solid";

import Button from "../button";
Expand Down Expand Up @@ -35,13 +36,13 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
const [pendingReason, setPendingReason] = createSignal<"switch" | null>(null);

const path = createMemo(() => props.path?.trim() ?? "");
const title = createMemo(() => (path() ? basename(path()) : "Artifact"));
const title = createMemo(() => (path() ? basename(path()) : t("session.editor_default_title")));
const dirty = createMemo(() => draft() !== original());
const canWrite = createMemo(() => Boolean(props.client && props.workspaceId));
const canSave = createMemo(() => dirty() && !saving() && canWrite());
const writeDisabledReason = createMemo(() => {
if (canWrite()) return null;
return "Connect to an OpenWork server worker to edit files.";
return t("session.editor_connect_hint");
});

const resetState = () => {
Expand Down Expand Up @@ -71,7 +72,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
return;
}
if (!isMarkdown(target)) {
setError("Only markdown files are supported.");
setError(t("session.editor_toast_markdown_only"));
return;
}

Expand Down Expand Up @@ -102,7 +103,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
result = (await client.readWorkspaceFile(workspaceId, actualPath)) as OpenworkWorkspaceFileContent;
} catch (second) {
if (second instanceof OpenworkServerError && second.status === 404) {
throw new OpenworkServerError(404, "file_not_found", "File not found (workspace root or outbox).");
throw new OpenworkServerError(404, "file_not_found", t("session.editor_toast_not_found"));
}
throw second;
}
Expand All @@ -114,7 +115,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
setResolvedPath(actualPath);
setBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load file";
const message = err instanceof Error ? err.message : t("session.editor_toast_load_failed");
setError(message);
setLoadedPath(target);
} finally {
Expand All @@ -127,11 +128,11 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
const workspaceId = props.workspaceId;
const target = resolvedPath() ?? path();
if (!client || !workspaceId || !target) {
props.onToast?.("Cannot save: OpenWork server not connected");
props.onToast?.(t("session.editor_toast_save_connect"));
return;
}
if (!isMarkdown(target)) {
props.onToast?.("Only markdown files are supported");
props.onToast?.(t("session.editor_toast_markdown_only"));
return;
}
if (!dirty()) return;
Expand Down Expand Up @@ -160,7 +161,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
setConfirmOverwrite(true);
return;
}
const message = err instanceof Error ? err.message : "Failed to save";
const message = err instanceof Error ? err.message : t("session.editor_toast_save_failed");
setError(message);
props.onToast?.(message);
} finally {
Expand All @@ -185,7 +186,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
return;
}
// Reload is destructive; reuse the close-discard banner semantics.
setError("Discard changes to reload from disk (close and reopen), or save first.");
setError(t("session.editor_toast_reload_discard"));
};

createEffect(() => {
Expand Down Expand Up @@ -241,7 +242,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
<div class="text-sm font-semibold text-dls-text truncate">{title()}</div>
<Show when={dirty()}>
<span class="text-[10px] px-2 py-0.5 rounded-full border border-amber-7/40 bg-amber-2/30 text-amber-11">
Unsaved
{t("session.editor_unsaved")}
</span>
</Show>
</div>
Expand All @@ -257,26 +258,26 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
class="text-xs h-9 py-0 px-3"
onClick={requestReload}
disabled={loading() || saving()}
title="Reload from disk"
title={t("session.editor_reload_tooltip")}
>
<RefreshCcw size={14} class={loading() ? "animate-spin" : ""} />
Reload
{t("session.editor_reload")}
</Button>
<Button
class="text-xs h-9 py-0 px-3"
onClick={() => void save()}
disabled={!canSave()}
title={writeDisabledReason() ?? "Save (Ctrl/Cmd+S)"}
title={writeDisabledReason() ?? t("session.editor_save_tooltip")}
>
<Save size={14} class={saving() ? "animate-pulse" : ""} />
{saving() ? "Saving..." : "Save"}
{saving() ? t("session.editor_saving") : t("session.editor_save")}
</Button>
<button
type="button"
class="p-2 rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
onClick={requestClose}
title="Close"
aria-label="Close"
title={t("session.editor_close")}
aria-label={t("session.editor_close")}
>
<X size={16} />
</button>
Expand All @@ -301,10 +302,10 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp

<Show when={confirmOverwrite()}>
<div class="shrink-0 px-4 py-2 border-b border-dls-border bg-amber-2/20 text-amber-11 text-xs flex items-center justify-between gap-3">
<div class="min-w-0">File changed since load. Overwrite anyway?</div>
<div class="min-w-0">{t("session.editor_overwrite_prompt")}</div>
<div class="shrink-0 flex items-center gap-2">
<Button variant="outline" class="text-xs h-8 py-0 px-3" onClick={() => setConfirmOverwrite(false)}>
Cancel
{t("common.cancel")}
</Button>
<Button
variant="danger"
Expand All @@ -314,18 +315,18 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
void save({ force: true });
}}
>
Overwrite
{t("session.editor_overwrite")}
</Button>
</div>
</div>
</Show>

<Show when={confirmDiscardClose()}>
<div class="shrink-0 px-4 py-2 border-b border-dls-border bg-amber-2/20 text-amber-11 text-xs flex items-center justify-between gap-3">
<div class="min-w-0">Discard unsaved changes and close?</div>
<div class="min-w-0">{t("session.editor_discard_prompt")}</div>
<div class="shrink-0 flex items-center gap-2">
<Button variant="outline" class="text-xs h-8 py-0 px-3" onClick={() => setConfirmDiscardClose(false)}>
Keep
{t("session.editor_keep")}
</Button>
<Button
variant="secondary"
Expand All @@ -336,7 +337,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
props.onClose();
}}
>
Discard
{t("session.editor_discard")}
</Button>
</div>
</div>
Expand All @@ -345,7 +346,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
<Show when={pendingPath() && pendingReason() === "switch"}>
<div class="shrink-0 px-4 py-2 border-b border-dls-border bg-amber-2/20 text-amber-11 text-xs flex items-center justify-between gap-3">
<div class="min-w-0 truncate" title={pendingPath() ?? ""}>
Switch to {pendingPath()}
{t("session.editor_switch_prompt").replace("{path}", pendingPath() ?? "")}
</div>
<div class="shrink-0 flex items-center gap-2">
<Button
Expand All @@ -356,7 +357,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
setPendingReason(null);
}}
>
Cancel
{t("common.cancel")}
</Button>
<Button
variant="secondary"
Expand All @@ -370,10 +371,10 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
if (next) void load(next);
}}
>
Discard & switch
{t("session.editor_discard_switch")}
</Button>
<Button class="text-xs h-8 py-0 px-3" onClick={() => void save()} disabled={!canSave()}>
Save & switch
{t("session.editor_save_switch")}
</Button>
</div>
</div>
Expand All @@ -384,7 +385,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp
value={draft()}
onChange={setDraft}
placeholder=""
ariaLabel="Artifact editor"
ariaLabel={t("session.editor_aria_label")}
class="h-full"
autofocus
/>
Expand Down
9 changes: 5 additions & 4 deletions packages/app/src/app/components/session/artifacts-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { For, Show, createMemo, createSignal } from "solid-js";
import { t } from "../../../i18n";
import { Paperclip } from "lucide-solid";

export type ArtifactsPanelProps = {
Expand Down Expand Up @@ -99,7 +100,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
<Paperclip size={14} class="text-dls-secondary" />
<div class="min-w-0">
<div class="text-[11px] font-bold tracking-tight text-dls-secondary uppercase">
Artifacts
{t("session.artifacts_title")}
</div>
</div>
</div>
Expand All @@ -111,7 +112,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
<div class="mt-2 space-y-1">
<Show
when={visibleArtifacts().length > 0}
fallback={<div class="text-xs text-dls-secondary px-1 py-1">No artifacts yet.</div>}
fallback={<div class="text-xs text-dls-secondary px-1 py-1">{t("session.artifacts_empty")}</div>}
>
<For each={visibleArtifacts()}>
{(artifact) => {
Expand All @@ -123,7 +124,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
const openable = () => (md() ? canOpenMarkdown() : img() ? canOpenImage() : false);
const tooltip = () => {
if (md()) return display();
if (img() && !canOpenImage()) return `${display()} (image preview coming soon)`;
if (img() && !canOpenImage()) return `${display()} (${t("session.artifacts_image_preview_soon")})`;
return display();
};
return (
Expand Down Expand Up @@ -173,7 +174,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
class="w-full mt-1 rounded-lg px-2 py-1.5 text-xs text-dls-secondary hover:text-dls-text hover:bg-dls-active transition-colors"
onClick={() => setShowAll((prev) => !prev)}
>
{showAll() ? "Show fewer" : `Show ${hiddenCount()} more`}
{showAll() ? t("session.artifacts_show_fewer") : t("session.artifacts_show_more").replace("{count}", String(hiddenCount()))}
</button>
</Show>
</div>
Expand Down
Loading