diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7031bc..bbd850c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to Termic, newest first. This file is the human-authored
source of truth: the in-app Update card and the /changelog page on termic.dev
are generated from it. See the `release` skill for how entries are added.
+## [Unreleased]
+
+Closed tabs and archived tasks are one click away instead of buried in History.
+
+### Features
+- Surface closed sessions in the "+" menu: the tab strip's "+" lists recently closed secondary agent tabs, letting you resume the exact same session instead of losing it for good; the sidebar's project "+" does the same for recently archived tasks. (#77)
+- Closing a non-shell terminal or agent tab can now skip the confirmation dialog, via the dialog's "Don't ask again" checkbox or the new "Confirm before closing an agent tab" toggle in Settings > General, once the new Resume section has you covered. A toast then points back to it.
+
## [0.20.1] - 2026-07-10
Workspaces are now Tasks, and you can create one in two clicks from the sidebar.
diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx
index ad023dd..3f14e54 100644
--- a/src/components/settings/GeneralSection.tsx
+++ b/src/components/settings/GeneralSection.tsx
@@ -49,6 +49,8 @@ export function GeneralSection() {
const setSettledHighlight = usePrefs(s => s.setSettledHighlight);
const workingIndicator = usePrefs(s => s.workingIndicator);
const setWorkingIndicator = usePrefs(s => s.setWorkingIndicator);
+ const confirmBeforeCloseAgentTab = usePrefs(s => s.confirmBeforeCloseAgentTab);
+ const setConfirmBeforeCloseAgentTab = usePrefs(s => s.setConfirmBeforeCloseAgentTab);
const loadRemoteImages = usePrefs(s => s.loadRemoteImages);
const setLoadRemoteImages = usePrefs(s => s.setLoadRemoteImages);
const globalDefaultSandbox = usePrefs(s => s.globalDefaultSandbox);
@@ -281,6 +283,15 @@ export function GeneralSection() {
/>
+
+
+
+
` at the call site; this component renders only
// the items (so the caller can also customize positioning).
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { useApp } from "@/store/app";
import { useUI } from "@/store/ui";
import { visibleCliIds } from "@/lib/agents";
import { createQuickTask, readNewTaskMode, writeNewTaskMode, type NewTaskMode } from "@/lib/quickTask";
-import { CliIcon, CLI_BRAND_COLOR } from "@/icons/cli";
-import { DropdownItem, DropdownSeparator } from "@/components/ui/Dropdown";
+import { taskRestore } from "@/lib/ipc";
+import { CliIcon, CLI_BRAND_COLOR, resolveIconId } from "@/icons/cli";
+import { DropdownItem, DropdownLabel, DropdownSeparator } from "@/components/ui/Dropdown";
import { GitBranch, GitBranchPlus, Link2, TerminalSquare, SquareChevronRight, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
+/** Coarse relative label for an archived-task timestamp. Unlike the tab
+ * strip's Resume entries (always seconds/minutes old), a task can sit
+ * archived for a long time, so this scales from minutes up through a
+ * short date instead of capping at hours. */
+function relativeArchivedTime(iso: string): string {
+ const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
+ if (mins < 1) return "just now";
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ if (days < 7) return `${days}d ago`;
+ return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(new Date(iso));
+}
+
/** Small section header: uppercase label + one-line explanation. Used for
* the non-git "RUN IN FOLDER" case, where there's no worktree/main choice
* to make. Not a dropdown menu item — pure visual, doesn't trap focus. */
@@ -42,6 +58,21 @@ export function ProjectActionsMenuItems({ projectId, onPick }: {
const detectedClis = useApp(s => s.detectedClis);
const openNewTask = useUI(s => s.openNewTask);
const openCustomCommand = useUI(s => s.openCustomCommand);
+ const setActiveTask = useApp(s => s.setActiveTask);
+ const loadAll = useApp(s => s.loadAll);
+ const setView = useApp(s => s.setView);
+ const tasks = useApp(s => s.tasks);
+ // Recently archived tasks for THIS project, most-recent first — same
+ // sort HistoryView uses, just capped and scoped to one project so the
+ // launcher menu can offer a one-click shortcut back into one instead of
+ // making the user leave to the full History page.
+ const archivedTasks = useMemo(
+ () => tasks
+ .filter(t => t.project_id === projectId && t.archived)
+ .sort((a, b) => (b.archived_at ?? b.created).localeCompare(a.archived_at ?? a.created))
+ .slice(0, 5),
+ [tasks, projectId],
+ );
const project = useApp(s => s.projects.find(p => p.id === projectId));
const isMulti = (project?.type ?? "single") === "multi";
// Non-git projects (issue #4) have no branches / worktrees — the only way
@@ -194,6 +225,41 @@ export function ProjectActionsMenuItems({ projectId, onPick }: {
)}
+ {/* Recently archived tasks for this project — a shortcut to
+ HistoryView's restore (same task_restore IPC + setActiveTask)
+ without leaving the sidebar. "More…" hands off to the full page
+ for anything past the first few. */}
+ {archivedTasks.length > 0 && (
+ <>
+
+ Resume
+ {archivedTasks.map(t => {
+ const iconId = resolveIconId(t.cli, agents);
+ return (
+ {
+ try {
+ const restored = await taskRestore(t.id);
+ await loadAll();
+ setActiveTask(restored.id);
+ } catch (err) {
+ console.error("task_restore failed:", err);
+ }
+ }}>
+
+
+
+
+
{t.name}
+
{relativeArchivedTime(t.archived_at ?? t.created)}
+
+
+ );
+ })}
+ setView("history")}>
+ More…
+
+ >
+ )}
>
);
}
diff --git a/src/components/task/TabBar.tsx b/src/components/task/TabBar.tsx
index dd9ac67..f971398 100644
--- a/src/components/task/TabBar.tsx
+++ b/src/components/task/TabBar.tsx
@@ -2,7 +2,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import type { Task, Tab, TerminalTab, Agent } from "@/lib/types";
-import { useApp, useTaskTabs, useActiveTabId } from "@/store/app";
+import { useApp, useTaskTabs, useActiveTabId, type ClosedTabEntry } from "@/store/app";
import { getAllLeaves } from "@/lib/splitTree";
import { useTabStripDrag } from "./useTabStripDrag";
import { Button } from "@/components/ui/Button";
@@ -22,6 +22,51 @@ import { fileIconUrl } from "@/lib/explorer/iconResolver";
const CLIS = ["claude", "codex", "agy", "grok", "opencode"] as const;
+// Stable reference for the "no closed tabs yet" case. `s.closedTabs[task.id]
+// ?? []` would mint a NEW array on every selector call, which Zustand's
+// default Object.is comparison treats as "changed" — re-rendering TabBar on
+// every unrelated store write (PTY output ticks etc) and, worse, feeding a
+// runaway render loop. A shared empty array keeps the reference stable.
+const NO_CLOSED_TABS: ClosedTabEntry[] = [];
+
+/** Coarse "Nm ago" label for a closed-tab timestamp. Closed tabs are
+ * always recent (session-only list), so minute/hour granularity is
+ * enough — no need for History's day/week/month buckets. */
+function relativeTime(iso: string): string {
+ const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
+ if (mins < 1) return "just now";
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ return `${Math.floor(hours / 24)}d ago`;
+}
+
+/** "Resume" section rows — recently closed secondary agent tabs (see
+ * `ClosedTabEntry`). Icon uses the same resolveIconId/CLI_BRAND_COLOR
+ * pairing as TabPill so a resumed tab's row matches the tab it becomes. */
+function ResumeMenuItems({ entries, agents, onResume }: {
+ entries: ClosedTabEntry[]; agents: Agent[]; onResume: (entryId: string) => void;
+}) {
+ return (
+ <>
+ {entries.map(entry => {
+ const iconId = resolveIconId(entry.cli, agents);
+ return (
+ onResume(entry.id)}>
+
+
+
+
+
{entry.title}
+
{relativeTime(entry.closedAt)}
+
+
+ );
+ })}
+ >
+ );
+}
+
/** Registry entries rendered as dropdown rows — shared by the main strip's
* and the right strip's + menus (both their "New terminal" custom entries
* and their "New agent" lists) so the two menus can't drift apart. */
@@ -72,6 +117,9 @@ export function TabBar({ task }: { task: Task }) {
[registry],
);
const openBroadcast = useUI(s => s.openBroadcast);
+ const closedTabs = useApp(s => s.closedTabs[task.id] ?? NO_CLOSED_TABS);
+ const resumeClosedTab = useApp(s => s.resumeClosedTab);
+ const setView = useApp(s => s.setView);
const [open, setOpen] = useState(false);
const suppressDropdownReturn = useRef(false);
const [renaming, setRenaming] = useState<{ id: string; value: string } | null>(null);
@@ -133,6 +181,15 @@ export function TabBar({ task }: { task: Task }) {
addAndFocusTab({ id: crypto.randomUUID(), type: "terminal", title: displayName, cli });
}
+ // Reopen a closed tab with its original session id (see resumeClosedTab
+ // in the store) — same dropdown-close/focus dance as addAndFocusTab,
+ // just driven by the store action instead of a locally-built tab.
+ function resumeAndFocus(entryId: string) {
+ suppressDropdownReturn.current = true;
+ resumeClosedTab(task.id, entryId);
+ setOpen(false);
+ }
+
/** Plain login-shell tab. Always uncaged: only agents run inside the
* task's seatbelt (see ShellTerminalItem / TerminalPane spawn). */
function spawnShellTab() {
@@ -205,6 +262,16 @@ export function TabBar({ task }: { task: Task }) {
New agent
visibleClis.has(a.id))} onSpawn={spawnTab} />
+ {closedTabs.length > 0 && (
+ <>
+
+ Resume
+
+ { setOpen(false); setView("history"); }}>
+ More…
+
+ >
+ )}
diff --git a/src/lib/closeTab.ts b/src/lib/closeTab.ts
index 39b3ba1..dce5eac 100644
--- a/src/lib/closeTab.ts
+++ b/src/lib/closeTab.ts
@@ -8,6 +8,7 @@
import { useApp } from "@/store/app";
import { useUI } from "@/store/ui";
+import { usePrefs } from "@/store/prefs";
import type { Tab } from "@/lib/types";
import { agentDisplayName, isTerminalCli } from "@/lib/agents";
@@ -41,6 +42,12 @@ async function confirmTabClose(tab: Tab | undefined, paneTab: boolean): Promise<
// terminal-like tabs have no session to lose either. Close silently
// instead of a fake "Stops the running process" confirm.
if (termLike && !tab.ptyId) return true;
+ // Fast path: the user opted out via the dialog's "Don't ask again"
+ // checkbox below. The "+" menu's Resume section (backed by closedTabs)
+ // makes undoing a close one click away, so a blocking modal on every
+ // close is no longer the only safety net. requestCloseTab /
+ // requestClosePaneTab toast a Resume shortcut once the close lands.
+ if (!usePrefs.getState().confirmBeforeCloseAgentTab) return true;
const label = tab.cli === "custom"
? (tab.title || "this command")
: agentDisplayName(tab.cli, useApp.getState().agents);
@@ -54,18 +61,65 @@ async function confirmTabClose(tab: Tab | undefined, paneTab: boolean): Promise<
: "Ends this agent's session. It won't be restored when the task reopens.",
confirmLabel: "Close tab",
destructive: !isMain && !termLike,
+ checkbox: { label: "Don't ask again", defaultValue: false },
});
- return ok === true;
+ // Only persist the opt-out when the user actually confirmed the close —
+ // ticking the box then backing out (Escape / Cancel / click-outside)
+ // still resolves with ok.checked=true (ConfirmDialog reports whatever
+ // the checkbox state was at dismissal), so gating on confirmed too
+ // stops a cancelled close from silently disabling future confirmations.
+ if (ok.confirmed && ok.checked) usePrefs.getState().setConfirmBeforeCloseAgentTab(false);
+ return ok.confirmed;
}
return true;
}
+/** After a fast-path close (confirm skipped, see above), tell the user
+ * where the tab went. Secondary tabs snapshot into `closedTabs` on close
+ * (see app.ts's closeTab) so the toast's action can reopen the exact one
+ * that was just closed; the main tab already auto-resumes on its own, so
+ * it gets an explanatory toast with no action. No-op for shells (closed
+ * silently, nothing to report) and edit tabs (own confirm path, unrelated
+ * to this pref). */
+function toastClosedTab(taskId: string, tab: Tab, paneTab: boolean) {
+ if (tab.type !== "terminal" || tab.cli === "shell") return;
+ const label = tab.cli === "custom"
+ ? (tab.title || "this command")
+ : agentDisplayName(tab.cli, useApp.getState().agents);
+ // Pane tabs are never snapshotted into closedTabs (see app.ts's closeTab) —
+ // there's nothing to point the user back to, so just confirm the close.
+ if (paneTab) {
+ useUI.getState().pushToast(`Closed "${label}".`, "info");
+ return;
+ }
+ const resumable = !tab.is_default;
+ if (!resumable) {
+ useUI.getState().pushToast(`Closed "${label}". It resumes automatically when you reopen this task.`, "info");
+ return;
+ }
+ // Bind THIS close's entry now (toastClosedTab runs synchronously right
+ // after closeTab, so closedTabs[taskId][0] is guaranteed to be it) rather
+ // than re-deriving "the latest entry" at click time — otherwise a second
+ // close (or a menu Resume) within the toast's ttl would make this button
+ // reopen the wrong tab. resumeClosedTab no-ops if the id is already gone.
+ const entryId = useApp.getState().closedTabs[taskId]?.[0]?.id;
+ useUI.getState().pushToast(`Closed "${label}". Resume it from the + menu.`, "info", {
+ ttlMs: 6000,
+ action: {
+ label: "Resume",
+ onClick: () => { if (entryId) useApp.getState().resumeClosedTab(taskId, entryId); },
+ },
+ });
+}
+
/** Close a main-pane tab, asking first when closing is destructive.
* Resolves once the tab is closed or the user backs out. */
export async function requestCloseTab(taskId: string, tabId: string) {
const tab = useApp.getState().tabs[taskId]?.find(t => t.id === tabId);
if (!(await confirmTabClose(tab, false))) return;
+ const fastClose = tab?.type === "terminal" && tab.cli !== "shell" && !usePrefs.getState().confirmBeforeCloseAgentTab;
useApp.getState().closeTab(taskId, tabId);
+ if (fastClose && tab) toastClosedTab(taskId, tab, false);
}
/** Close a split-pane tab with the same confirm gate as the main path.
@@ -75,6 +129,8 @@ export async function requestCloseTab(taskId: string, tabId: string) {
export async function requestClosePaneTab(taskId: string, paneId: string, tabId: string): Promise {
const tab = useApp.getState().tabs[taskId]?.find(t => t.id === tabId);
if (!(await confirmTabClose(tab, true))) return false;
+ const fastClose = tab?.type === "terminal" && tab.cli !== "shell" && !usePrefs.getState().confirmBeforeCloseAgentTab;
useApp.getState().closePaneTab(taskId, paneId, tabId);
+ if (fastClose && tab) toastClosedTab(taskId, tab, true);
return true;
}
diff --git a/src/store/app.ts b/src/store/app.ts
index d3baa20..d88aa9b 100644
--- a/src/store/app.ts
+++ b/src/store/app.ts
@@ -12,6 +12,24 @@ import * as ipc from "@/lib/ipc";
import { focusTerminalTab, focusMainTab, focusPaneTab } from "@/lib/tabFocus";
import { agentDisplayName } from "@/lib/agents";
+/** A secondary agent tab closed via the "X", snapshotted just before it's
+ * dropped from `persisted_tabs` (see `syncDurableTabs`'s forget rule).
+ * Powers the "+" menu's Resume section — in-memory only, cleared on app
+ * restart. Main/default tabs are excluded: they already auto-resume via
+ * `persisted_tabs` when the task wakes, so listing them here would
+ * be redundant. */
+export interface ClosedTabEntry {
+ id: string;
+ cli: string;
+ title: string;
+ customTitle?: boolean;
+ command?: string | null;
+ sessionId?: string | null;
+ closedAt: string;
+}
+
+const MAX_CLOSED_TABS = 6;
+
interface View {
/** Underlying page — dashboard / history / empty. NOT "settings": Settings
* is a separate overlay flag (`settingsOpen`), so closing it returns to
@@ -40,6 +58,9 @@ export interface AppState {
tabs: Record;
/** task id → active tab id */
activeTab: Record;
+ /** task id → recently closed secondary agent tabs, most-recent
+ * first, capped at MAX_CLOSED_TABS. See `ClosedTabEntry`. */
+ closedTabs: Record;
view: View;
compactSidebar: boolean;
rightPanelHidden: boolean;
@@ -226,6 +247,11 @@ export interface AppState {
* No-op if the order is unchanged. */
reorderTab: (taskId: string, tabId: string, toIndex: number) => void;
closeTab: (taskId: string, tabId: string) => void;
+ /** Reopen a `closedTabs` entry as a fresh tab, forcing its original
+ * `sessionId` so the agent resumes via `--resume ` (see
+ * `decideResume` in TerminalPane) instead of starting a new
+ * conversation. Removes the entry from `closedTabs` once reopened. */
+ resumeClosedTab: (taskId: string, entryId: string) => void;
setActiveTabId: (taskId: string, tabId: string) => void;
persistTab: (taskId: string, tabId: string) => void;
openPreviewTab: (taskId: string, data: { type: "edit" | "diff"; path: string; title: string; revealAt?: { line: number; col?: number }; revealHeading?: string }) => void;
@@ -349,6 +375,7 @@ export const useApp = create((set, get) => ({
activeTaskId: null,
tabs: {},
activeTab: {},
+ closedTabs: {},
fsRevision: {},
view: { page: "dashboard" },
compactSidebar: initialCompact,
@@ -1464,6 +1491,24 @@ export const useApp = create((set, get) => ({
const closing = list[idx];
// Best-effort PTY kill; ignore failures (already-dead PTYs etc.).
if (closing.type === "terminal" && closing.ptyId) ipc.ptyKill(closing.ptyId).catch(() => {});
+ // Snapshot secondary agent tabs into closedTabs before syncDurableTabs
+ // forgets them for good (see the merge rule below) — this is the only
+ // point their session_id survives, so the "+" menu's Resume section can
+ // still reopen them. Shells (no session) and the main tab (already
+ // auto-resumes via persisted_tabs) are excluded.
+ const closingTerm = closing.type === "terminal" ? closing as TerminalTab : null;
+ const closedEntry: ClosedTabEntry | null =
+ closingTerm && closingTerm.cli !== "shell" && !closingTerm.is_default && !closingTerm.paneId
+ ? {
+ id: crypto.randomUUID(),
+ cli: closingTerm.cli,
+ title: closingTerm.customTitle ? closingTerm.title : (closingTerm.liveTitle || closingTerm.title),
+ customTitle: closingTerm.customTitle,
+ command: closingTerm.command ?? null,
+ sessionId: closingTerm.sessionId ?? null,
+ closedAt: new Date().toISOString(),
+ }
+ : null;
const next = list.filter(t => t.id !== tabId);
const wasActive = s.activeTab[taskId] === tabId;
// Active-tab replacement considers only main tabs (no paneId).
@@ -1490,6 +1535,12 @@ export const useApp = create((set, get) => ({
splitTree: _st ? splitTreeRest : s.splitTree,
activePaneId: _ap ? activePaneRest : s.activePaneId,
paneHistory: _ph ? paneHistoryRest : s.paneHistory,
+ ...(closedEntry ? {
+ closedTabs: {
+ ...s.closedTabs,
+ [taskId]: [closedEntry, ...(s.closedTabs[taskId] ?? [])].slice(0, MAX_CLOSED_TABS),
+ },
+ } : {}),
};
if (isLast) {
// Evict from mountedTasks → TaskView unmounts → xterm
@@ -1523,6 +1574,24 @@ export const useApp = create((set, get) => ({
}
},
+ resumeClosedTab: (taskId, entryId) => {
+ const entry = (get().closedTabs[taskId] ?? []).find(e => e.id === entryId);
+ if (!entry) return;
+ set(s => ({
+ closedTabs: { ...s.closedTabs, [taskId]: (s.closedTabs[taskId] ?? []).filter(e => e.id !== entryId) },
+ }));
+ const tab: TerminalTab = {
+ id: crypto.randomUUID(),
+ type: "terminal",
+ title: entry.title,
+ customTitle: entry.customTitle,
+ cli: entry.cli,
+ command: entry.command ?? undefined,
+ sessionId: entry.sessionId ?? undefined,
+ };
+ get().addTab(taskId, tab);
+ },
+
setActiveTabId: (taskId, tabId) => set(s => {
// Looking at a tab = "I've seen this / I'm dealing with it now."
// Clear EVERY status flag on focus:
diff --git a/src/store/prefs.ts b/src/store/prefs.ts
index 8a6cf07..1ffe192 100644
--- a/src/store/prefs.ts
+++ b/src/store/prefs.ts
@@ -33,6 +33,7 @@ const LS_LIGATURES = "codeLigatures";
const LS_THEME = "themeMode";
const LS_DESKTOPNOTIF = "desktopNotifications";
const LS_SETTLED_HIGHLIGHT = "settledHighlight";
+const LS_CONFIRM_CLOSE_AGENT_TAB = "confirmBeforeCloseAgentTab";
const LS_WORKING_INDICATOR = "workingIndicator";
const LS_DEFAULT_SANDBOX = "globalDefaultSandbox";
const LS_SANDBOX_BYPASS = "sandboxBypassPermissions";
@@ -351,6 +352,13 @@ interface PrefsState {
* in-app "done" signal. Some users find it distracting and want
* the sidebar to stay calm regardless. */
settledHighlight: boolean;
+ /** Whether closing a non-shell terminal/agent tab asks for confirmation
+ * first. ON by default. Once the "+" menu's Resume section makes
+ * undoing a close one click away, users who've learned that can turn
+ * this off via the dialog's "Don't ask again" checkbox — closing then
+ * happens immediately, with a toast pointing back at Resume. Dirty
+ * edit-tab closes are never gated by this; that confirm always fires. */
+ confirmBeforeCloseAgentTab: boolean;
/** Show a spinner on an agent's tab (and sidebar icon) WHILE it's
* working. OFF by default — experimental. The "working" workState is
* always tracked internally to drive work-done detection; this pref only
@@ -497,6 +505,7 @@ interface PrefsState {
setCompletionSound: (v: boolean) => void;
setCompletionSoundId: (id: CompletionSoundId) => void;
setSettledHighlight: (v: boolean) => void;
+ setConfirmBeforeCloseAgentTab: (v: boolean) => void;
setWorkingIndicator: (v: boolean) => void;
setLoadRemoteImages: (v: boolean) => void;
setGlobalDefaultSandbox: (v: boolean) => void;
@@ -599,6 +608,7 @@ const initialCompletionSoundId = readCompletionSoundId();
// users who toggled it OFF keep their setting (lsGetBool returns the
// stored value when present).
const initialSettledHighlight = lsGetBool(LS_SETTLED_HIGHLIGHT, true);
+const initialConfirmCloseAgentTab = lsGetBool(LS_CONFIRM_CLOSE_AGENT_TAB, true);
// OFF by default — experimental re-introduction of the work-in-progress
// spinner. Opt in via Settings → General.
const initialWorkingIndicator = lsGetBool(LS_WORKING_INDICATOR, false);
@@ -635,6 +645,7 @@ export const usePrefs = create(set => ({
completionSound: initialCompletionSound,
completionSoundId: initialCompletionSoundId,
settledHighlight: initialSettledHighlight,
+ confirmBeforeCloseAgentTab: initialConfirmCloseAgentTab,
workingIndicator: initialWorkingIndicator,
loadRemoteImages: initialLoadRemoteImages,
globalDefaultSandbox: initialDefaultSandbox,
@@ -780,6 +791,10 @@ export const usePrefs = create(set => ({
try { localStorage.setItem(LS_SETTLED_HIGHLIGHT, v ? "1" : "0"); } catch {}
set({ settledHighlight: v });
},
+ setConfirmBeforeCloseAgentTab: (v) => {
+ try { localStorage.setItem(LS_CONFIRM_CLOSE_AGENT_TAB, v ? "1" : "0"); } catch {}
+ set({ confirmBeforeCloseAgentTab: v });
+ },
setWorkingIndicator: (v) => {
try { localStorage.setItem(LS_WORKING_INDICATOR, v ? "1" : "0"); } catch {}
set({ workingIndicator: v });