From 80147c9900bb8ea275d1405ffa4163eccfa784ae Mon Sep 17 00:00:00 2001
From: iliya
Date: Thu, 5 Mar 2026 18:57:07 +0200
Subject: [PATCH] feat: update package version and add linting dependency
- Bumped package version from 0.1.0 to 1.0.0 to reflect significant updates.
- Added @codemirror/lint dependency to enhance code linting capabilities.
- Updated pnpm-lock.yaml to include the new linting dependency version.
---
package.json | 3 +-
pnpm-lock.yaml | 11 +-
.../services/infrastructure/UpdaterService.ts | 2 +
.../services/team/TeamProvisioningService.ts | 58 ++-
.../components/chat/DisplayItemList.tsx | 1 +
.../components/chat/items/BaseItem.tsx | 2 +-
.../components/chat/items/LinkedToolItem.tsx | 12 +-
.../components/chat/items/TextItem.tsx | 8 +-
.../components/chat/items/ThinkingItem.tsx | 8 +-
.../components/chat/searchHighlightUtils.ts | 17 +-
.../chat/viewers/MarkdownViewer.tsx | 4 +
.../components/common/WarningBanner.tsx | 25 ++
.../components/layout/SidebarHeader.tsx | 8 +-
.../components/layout/SortableTab.tsx | 17 +-
src/renderer/components/layout/TabBar.tsx | 7 +-
.../components/TriggerPreview.tsx | 9 +-
.../settings/sections/AdvancedSection.tsx | 23 +-
.../settings/sections/CliStatusSection.tsx | 17 +-
.../settings/sections/ConfigEditorDialog.tsx | 402 ++++++++++++++++++
.../settings/sections/GeneralSection.tsx | 17 +-
.../components/team/CliLogsRichView.tsx | 7 +-
.../team/ProvisioningProgressBlock.tsx | 8 +-
.../components/team/TeamDetailView.tsx | 19 +-
src/renderer/components/team/TeamListView.tsx | 4 +
.../team/TeamProvisioningBanner.tsx | 12 +-
.../attachments/AttachmentPreviewList.tsx | 9 +-
.../team/dialogs/CreateTaskDialog.tsx | 13 +-
.../team/dialogs/CreateTeamDialog.tsx | 30 +-
.../team/dialogs/ExtendedContextCheckbox.tsx | 15 +-
.../team/dialogs/LaunchTeamDialog.tsx | 34 +-
.../team/dialogs/ProjectPathSelector.tsx | 2 +-
.../team/editor/ProjectEditorOverlay.tsx | 9 +-
.../team/messages/MessageComposer.tsx | 2 +-
src/renderer/index.css | 28 ++
src/renderer/store/slices/updateSlice.ts | 1 +
35 files changed, 757 insertions(+), 87 deletions(-)
create mode 100644 src/renderer/components/common/WarningBanner.tsx
create mode 100644 src/renderer/components/settings/sections/ConfigEditorDialog.tsx
diff --git a/package.json b/package.json
index 99fe397f..4db5dd7d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "claude-agent-teams-ui",
"type": "module",
- "version": "0.1.0",
+ "version": "1.0.0",
"description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls",
"license": "AGPL-3.0",
"author": {
@@ -80,6 +80,7 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.12.1",
"@codemirror/language-data": "^6.5.2",
+ "@codemirror/lint": "^6.9.5",
"@codemirror/merge": "^6.12.0",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f916640..147ae6f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -68,6 +68,9 @@ importers:
'@codemirror/language-data':
specifier: ^6.5.2
version: 6.5.2
+ '@codemirror/lint':
+ specifier: ^6.9.5
+ version: 6.9.5
'@codemirror/merge':
specifier: ^6.12.0
version: 6.12.0
@@ -576,8 +579,8 @@ packages:
'@codemirror/legacy-modes@6.5.2':
resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==}
- '@codemirror/lint@6.9.4':
- resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
+ '@codemirror/lint@6.9.5':
+ resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
'@codemirror/merge@6.12.0':
resolution: {integrity: sha512-o+36bbapcEHf4Ux75pZ4CKjMBUd14parA0uozvWVlacaT+uxaA3DDefEvWYjngsKU+qsrDe/HOOfsw0Q72pLjA==}
@@ -6449,7 +6452,7 @@ snapshots:
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
- '@codemirror/lint': 6.9.4
+ '@codemirror/lint': 6.9.5
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@lezer/common': 1.5.1
@@ -6609,7 +6612,7 @@ snapshots:
dependencies:
'@codemirror/language': 6.12.1
- '@codemirror/lint@6.9.4':
+ '@codemirror/lint@6.9.5':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
diff --git a/src/main/services/infrastructure/UpdaterService.ts b/src/main/services/infrastructure/UpdaterService.ts
index ec018951..9fb74ee4 100644
--- a/src/main/services/infrastructure/UpdaterService.ts
+++ b/src/main/services/infrastructure/UpdaterService.ts
@@ -41,6 +41,7 @@ export class UpdaterService {
await autoUpdater.checkForUpdates();
} catch (error) {
logger.error('Check for updates failed:', getErrorMessage(error));
+ this.sendStatus({ type: 'error', error: getErrorMessage(error) });
}
}
@@ -52,6 +53,7 @@ export class UpdaterService {
await autoUpdater.downloadUpdate();
} catch (error) {
logger.error('Download update failed:', getErrorMessage(error));
+ this.sendStatus({ type: 'error', error: getErrorMessage(error) });
}
}
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts
index f664304c..71a7055a 100644
--- a/src/main/services/team/TeamProvisioningService.ts
+++ b/src/main/services/team/TeamProvisioningService.ts
@@ -161,6 +161,8 @@ interface ProvisioningRun {
* request triggered by the UI. We suppress any lead→user echo for that turn.
*/
silentUserDmForward: { target: string; startedAt: string } | null;
+ /** Safety valve: clears silentUserDmForward if turn never completes. */
+ silentUserDmForwardClearHandle: NodeJS.Timeout | null;
/** Accumulates assistant text during provisioning phase for live UI preview. */
provisioningOutputParts: string[];
/** Session ID detected from stream-json output (result.session_id or message.session_id). */
@@ -659,8 +661,14 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string {
` - CRITICAL: Do NOT start working on the tasks now. Provisioning is ONLY for setting up the team structure.\n` +
` - The tasks will be executed after the team is launched separately.`
: `3) If user instructions explicitly ask to create tasks OR describe substantial/assigned work that should be tracked — create tasks on the team board.
- - Prefer fewer, broader tasks over many micro-tasks.
- - Avoid duplicate notifications for the same assignment.
+ - PRIORITY (delegation-first): your default behavior is to translate user requests into a task plan, create the tasks, and delegate them to teammates.
+ - Do NOT start executing/implementing tasks yourself in this turn.
+ - Do NOT “block” on doing the work before creating/assigning tasks — keep this turn fast so the user can send more instructions.
+ - Exception: only if the team is truly SOLO (no teammates) may you execute tasks yourself. This is NOT the case here.
+ - Decompose the request into a small set of clear, outcome-based tasks (prefer fewer, broader tasks over many micro-tasks).
+ - Assign each created task to an appropriate teammate as owner (NOT to yourself), based on role/workflow and current load.
+ - If ownership is unclear, pick the best default owner and note assumptions in the task description or a task comment.
+ - Avoid duplicate notifications for the same assignment (one message per member per topic is enough).
- When tasks have natural ordering (e.g. setup → implementation → testing), use --blocked-by.
- If a task is blocked (uses --blocked-by), it MUST be created as pending (use --status pending). Do NOT mark blocked tasks in_progress.
- Review guidance:
@@ -713,6 +721,7 @@ Constraints:
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
- Use the team task board for assigned/substantial work.
+- DELEGATION-FIRST (behavior rule for ALL future turns): When "user" gives you work, your top priority is to (a) decompose into tasks, (b) create tasks on the team board, (c) assign them to teammates, and (d) SendMessage "user" a short confirmation (task IDs + owners). Do NOT start implementing yourself unless the team is truly in SOLO MODE (no teammates).
- TaskCreate is optional for private planning only; do NOT use it for team-board tasks.
- When messaging "user" (the human): NEVER mention teamctl.js, internal scripts, CLI commands, or file paths under ~/.claude/. The user sees messages in the UI — write plain human language. If a task needs a status update, do it yourself via Bash; never ask the user to run a command.${soloConstraint}
@@ -875,6 +884,7 @@ Constraints:
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
- Use the team task board for assigned/substantial work.
+- DELEGATION-FIRST (behavior rule for ALL future turns): When "user" gives you work, your top priority is to (a) decompose into tasks, (b) create tasks on the team board, (c) assign them to teammates, and (d) SendMessage "user" a short confirmation (task IDs + owners). Do NOT start implementing yourself unless the team is truly in SOLO MODE (no teammates).
- TaskCreate is optional for private planning only; do NOT use it for team-board tasks.
- When messaging "user" (the human): NEVER mention teamctl.js, internal scripts, CLI commands, or file paths under ~/.claude/. The user sees messages in the UI — write plain human language. If a task needs a status update, do it yourself via Bash; never ask the user to run a command.${soloConstraint}
@@ -1684,6 +1694,7 @@ export class TeamProvisioningService {
leadRelayCapture: null,
directReplyParts: [],
silentUserDmForward: null,
+ silentUserDmForwardClearHandle: null,
provisioningOutputParts: [],
detectedSessionId: null,
leadActivityState: 'active',
@@ -1974,6 +1985,7 @@ export class TeamProvisioningService {
leadRelayCapture: null,
directReplyParts: [],
silentUserDmForward: null,
+ silentUserDmForwardClearHandle: null,
provisioningOutputParts: [],
detectedSessionId: null,
leadActivityState: 'active',
@@ -2243,20 +2255,34 @@ export class TeamProvisioningService {
}
run.silentUserDmForward = { target: teammateName, startedAt: nowIso() };
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
+ // Safety valve: if the CLI never emits a result message, don't stay in "silent" mode forever.
+ run.silentUserDmForwardClearHandle = setTimeout(() => {
+ run.silentUserDmForward = null;
+ run.silentUserDmForwardClearHandle = null;
+ }, 60_000);
+ run.silentUserDmForwardClearHandle.unref();
const summaryLine = userSummary?.trim() ? `Summary: ${userSummary.trim()}` : null;
+ const internal = wrapInAgentBlock(
+ [
+ `UI relay request — forward a direct message to teammate "${teammateName}".`,
+ `MUST: use the SendMessage tool with recipient="${teammateName}".`,
+ `MUST: ask the teammate to reply back to recipient "user" (short answer).`,
+ `CRITICAL: Do NOT send any message to recipient "user" for this turn.`,
+ ].join('\n')
+ );
const message = [
- `INTERNAL: The human user sent a direct message to teammate "${teammateName}" via the UI.`,
- `Action: forward it to that teammate using the SendMessage tool.`,
- `IMPORTANT: Do NOT reply to the human user for this turn.`,
- `In the forwarded message, ask the teammate to reply to recipient "user" with a short answer.`,
+ `User DM relay (internal).`,
+ internal,
``,
- `User message:`,
+ `Message to forward:`,
...(summaryLine ? [summaryLine] : []),
userText,
- ]
- .filter(Boolean)
- .join('\n');
+ ].join('\n');
await this.sendMessageToTeam(teamName, message);
}
@@ -2986,6 +3012,10 @@ export class TeamProvisioningService {
}
// Clear silent relay flag after any successful turn.
run.silentUserDmForward = null;
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
if (!run.provisioningComplete && !run.cancelRequested) {
void this.handleProvisioningTurnComplete(run);
}
@@ -2998,6 +3028,10 @@ export class TeamProvisioningService {
}
// Clear silent relay flag after any errored turn.
run.silentUserDmForward = null;
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
if (!run.provisioningComplete && !run.cancelRequested) {
const progress = updateProgress(
run,
@@ -3238,6 +3272,10 @@ export class TeamProvisioningService {
clearTimeout(run.timeoutHandle);
run.timeoutHandle = null;
}
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
this.stopFilesystemMonitor(run);
// Remove stream listeners to prevent data handlers firing on a cleaned-up run
if (run.child) {
diff --git a/src/renderer/components/chat/DisplayItemList.tsx b/src/renderer/components/chat/DisplayItemList.tsx
index ecd5a649..59dd25ae 100644
--- a/src/renderer/components/chat/DisplayItemList.tsx
+++ b/src/renderer/components/chat/DisplayItemList.tsx
@@ -162,6 +162,7 @@ export const DisplayItemList = ({
linkedTool={item.tool}
onClick={() => onItemClick(itemKey)}
isExpanded={expandedItemIds.has(itemKey)}
+ searchQueryOverride={searchQueryOverride}
isHighlighted={highlightToolUseId === item.tool.id}
highlightColor={highlightColor}
notificationDotColor={notificationColorMap?.get(item.tool.id)}
diff --git a/src/renderer/components/chat/items/BaseItem.tsx b/src/renderer/components/chat/items/BaseItem.tsx
index 66e772f7..e1a20ec0 100644
--- a/src/renderer/components/chat/items/BaseItem.tsx
+++ b/src/renderer/components/chat/items/BaseItem.tsx
@@ -18,7 +18,7 @@ interface BaseItemProps {
/** Primary label (e.g., "Thinking", "Output", tool name) */
label: string;
/** Summary text shown after the label */
- summary?: string;
+ summary?: React.ReactNode;
/** Token count to display */
tokenCount?: number;
/** Label for tokens (default: "tokens") */
diff --git a/src/renderer/components/chat/items/LinkedToolItem.tsx b/src/renderer/components/chat/items/LinkedToolItem.tsx
index c11798d1..2dabb345 100644
--- a/src/renderer/components/chat/items/LinkedToolItem.tsx
+++ b/src/renderer/components/chat/items/LinkedToolItem.tsx
@@ -38,6 +38,7 @@ import {
ToolErrorDisplay,
WriteToolViewer,
} from './linkedTool';
+import { highlightQueryInText } from '../searchHighlightUtils';
import type { LinkedToolItem as LinkedToolItemType } from '@renderer/types/groups';
@@ -45,6 +46,8 @@ interface LinkedToolItemProps {
linkedTool: LinkedToolItemType;
onClick: () => void;
isExpanded: boolean;
+ /** Optional local search query override for inline highlighting */
+ searchQueryOverride?: string;
/** Whether this item should be highlighted for error deep linking */
isHighlighted?: boolean;
/** Custom highlight color from trigger */
@@ -59,6 +62,7 @@ export const LinkedToolItem: React.FC = ({
linkedTool,
onClick,
isExpanded,
+ searchQueryOverride,
isHighlighted,
highlightColor,
notificationDotColor,
@@ -66,6 +70,12 @@ export const LinkedToolItem: React.FC = ({
}) => {
const status = getToolStatus(linkedTool);
const summary = getToolSummary(linkedTool.name, linkedTool.input);
+ const summaryNode =
+ searchQueryOverride && searchQueryOverride.trim().length > 0
+ ? highlightQueryInText(summary, searchQueryOverride, `${linkedTool.id ?? linkedTool.name}:summary`, {
+ forceAllActive: true,
+ })
+ : summary;
const elementRef = useRef(null);
// Combined ref callback - handles both internal ref and external registration
@@ -155,7 +165,7 @@ export const LinkedToolItem: React.FC = ({
/>
}
label={linkedTool.name}
- summary={summary}
+ summary={summaryNode}
tokenCount={getToolContextTokens(linkedTool)}
status={status}
durationMs={linkedTool.durationMs}
diff --git a/src/renderer/components/chat/items/TextItem.tsx b/src/renderer/components/chat/items/TextItem.tsx
index ea0cba41..d9f9ab5d 100644
--- a/src/renderer/components/chat/items/TextItem.tsx
+++ b/src/renderer/components/chat/items/TextItem.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { MessageSquare } from 'lucide-react';
import { MarkdownViewer } from '../viewers';
+import { highlightQueryInText } from '../searchHighlightUtils';
import { BaseItem } from './BaseItem';
import { truncateText } from './baseItemHelpers';
@@ -40,6 +41,11 @@ export const TextItem: React.FC = ({
}) => {
const fullContent = step.content.outputText ?? preview;
const truncatedPreview = truncateText(preview, 60);
+ const summary = searchQueryOverride
+ ? highlightQueryInText(truncatedPreview, searchQueryOverride, `${markdownItemId ?? step.id}:summary`, {
+ forceAllActive: true,
+ })
+ : truncatedPreview;
// Get token count from step.tokens.output or step.content.tokenCount
const tokenCount = step.tokens?.output ?? step.content.tokenCount ?? 0;
@@ -48,7 +54,7 @@ export const TextItem: React.FC = ({
}
label="Output"
- summary={truncatedPreview}
+ summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}
diff --git a/src/renderer/components/chat/items/ThinkingItem.tsx b/src/renderer/components/chat/items/ThinkingItem.tsx
index 34a6f50d..c3cdafad 100644
--- a/src/renderer/components/chat/items/ThinkingItem.tsx
+++ b/src/renderer/components/chat/items/ThinkingItem.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { Brain } from 'lucide-react';
import { MarkdownViewer } from '../viewers';
+import { highlightQueryInText } from '../searchHighlightUtils';
import { BaseItem } from './BaseItem';
import { truncateText } from './baseItemHelpers';
@@ -40,6 +41,11 @@ export const ThinkingItem: React.FC = ({
}) => {
const fullContent = step.content.thinkingText ?? preview;
const truncatedPreview = truncateText(preview, 60);
+ const summary = searchQueryOverride
+ ? highlightQueryInText(truncatedPreview, searchQueryOverride, `${markdownItemId ?? step.id}:summary`, {
+ forceAllActive: true,
+ })
+ : truncatedPreview;
// Get token count from step.tokens.output or step.content.tokenCount
const tokenCount = step.tokens?.output ?? step.content.tokenCount ?? 0;
@@ -48,7 +54,7 @@ export const ThinkingItem: React.FC = ({
}
label="Thinking"
- summary={truncatedPreview}
+ summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}
diff --git a/src/renderer/components/chat/searchHighlightUtils.ts b/src/renderer/components/chat/searchHighlightUtils.ts
index 873351b4..12ec37e2 100644
--- a/src/renderer/components/chat/searchHighlightUtils.ts
+++ b/src/renderer/components/chat/searchHighlightUtils.ts
@@ -35,6 +35,8 @@ export interface SearchContext {
matchCounter: { current: number };
isCurrentItem: boolean;
currentMatchIndexInItem: number | null;
+ /** When true, render all matches using the "current" highlight style */
+ forceAllActive?: boolean;
}
/**
@@ -79,7 +81,8 @@ function highlightSearchText(text: string, ctx: SearchContext): React.ReactNode
}
const isCurrentResult =
- ctx.isCurrentItem && ctx.currentMatchIndexInItem === ctx.matchCounter.current;
+ ctx.forceAllActive === true ||
+ (ctx.isCurrentItem && ctx.currentMatchIndexInItem === ctx.matchCounter.current);
parts.push(
React.createElement(
@@ -109,6 +112,18 @@ function highlightSearchText(text: string, ctx: SearchContext): React.ReactNode
return parts;
}
+export function highlightQueryInText(
+ text: string,
+ query: string,
+ itemId: string,
+ options?: { forceAllActive?: boolean }
+): React.ReactNode {
+ const ctx = createSearchContext(query, itemId, [], -1);
+ if (!ctx) return text;
+ if (options?.forceAllActive) ctx.forceAllActive = true;
+ return highlightSearchInChildren(text, ctx);
+}
+
/**
* Recursively process React children to highlight search terms in text nodes.
* Preserves the React element tree structure (markdown components, etc.)
diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
index fc5d4d41..25923608 100644
--- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx
+++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
@@ -600,6 +600,10 @@ export const MarkdownViewer: React.FC = ({
effectiveQuery && itemId
? createSearchContext(effectiveQuery, itemId, effectiveMatches, effectiveIndex)
: null;
+ // Local search (Claude logs): use bright highlight for all matches (no "current result" concept).
+ if (searchCtx && searchQueryOverride) {
+ searchCtx.forceAllActive = true;
+ }
// Create markdown components with optional search highlighting
// When search is active, create fresh each render (match counter is stateful and must start at 0)
diff --git a/src/renderer/components/common/WarningBanner.tsx b/src/renderer/components/common/WarningBanner.tsx
new file mode 100644
index 00000000..5bc540ac
--- /dev/null
+++ b/src/renderer/components/common/WarningBanner.tsx
@@ -0,0 +1,25 @@
+import { AlertTriangle } from 'lucide-react';
+
+interface WarningBannerProps {
+ children: React.ReactNode;
+ className?: string;
+ icon?: React.ReactNode;
+}
+
+export const WarningBanner = ({
+ children,
+ className = '',
+ icon,
+}: WarningBannerProps): React.JSX.Element => (
+
+ {icon ??
}
+
{children}
+
+);
diff --git a/src/renderer/components/layout/SidebarHeader.tsx b/src/renderer/components/layout/SidebarHeader.tsx
index b9629c89..4b631c7f 100644
--- a/src/renderer/components/layout/SidebarHeader.tsx
+++ b/src/renderer/components/layout/SidebarHeader.tsx
@@ -44,9 +44,11 @@ export const SidebarHeader = (): React.JSX.Element => {
} as React.CSSProperties
}
>
-
+ {isMacElectron && (
+
+ )}
{
+ if (tab.type !== 'team' || !tab.teamName) return null;
+ const team = s.teamByName[tab.teamName];
+ return team?.color ?? null;
+ });
+ const teamColorSet = teamColor ? getTeamColorSet(teamColor) : null;
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: tab.id,
data: {
@@ -81,13 +89,18 @@ export const SortableTab = ({
transition: isDragging ? 'none' : transition,
opacity: isDragging ? 0.3 : 1,
backgroundColor: isActive
- ? 'var(--color-surface-raised)'
+ ? teamColorSet
+ ? teamColorSet.badge
+ : 'var(--color-surface-raised)'
: isHovered
- ? 'var(--color-surface-overlay)'
+ ? teamColorSet
+ ? teamColorSet.badge
+ : 'var(--color-surface-overlay)'
: 'transparent',
color: isActive || isHovered ? 'var(--color-text)' : 'var(--color-text-muted)',
outline: isSelected ? '1px solid var(--color-border-emphasis)' : 'none',
outlineOffset: '-1px',
+ borderLeft: isActive && teamColorSet ? `2px solid ${teamColorSet.border}` : undefined,
};
const Icon = TAB_ICONS[tab.type];
diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx
index 893f27d6..4efd68ef 100644
--- a/src/renderer/components/layout/TabBar.tsx
+++ b/src/renderer/components/layout/TabBar.tsx
@@ -302,13 +302,14 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
scrollContainerRef.current = el;
setDroppableRef(el);
}}
- className="scrollbar-none flex min-w-0 shrink items-center gap-1 overflow-x-auto"
+ className="scrollbar-none flex min-w-0 flex-1 items-center gap-1"
style={
{
- maxWidth: '75%',
WebkitAppRegion: 'no-drag',
outline: isDroppableOver ? '1px dashed var(--color-accent, #6366f1)' : 'none',
outlineOffset: '-1px',
+ overflowX: 'auto',
+ overflowY: 'clip',
} as React.CSSProperties
}
>
@@ -351,7 +352,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
Gives users a reliable window-drag target regardless of how many tabs are open.
Only applied on the leftmost pane in Electron to match the TabBar drag region logic. */}
+
Search stopped early (timeout or count limit). Actual matches may be higher.
diff --git a/src/renderer/components/settings/sections/AdvancedSection.tsx b/src/renderer/components/settings/sections/AdvancedSection.tsx
index d42f97e8..9915f422 100644
--- a/src/renderer/components/settings/sections/AdvancedSection.tsx
+++ b/src/renderer/components/settings/sections/AdvancedSection.tsx
@@ -7,11 +7,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api, isElectronMode } from '@renderer/api';
import appIcon from '@renderer/favicon.png';
import { useStore } from '@renderer/store';
-import { CheckCircle, Code2, Download, Loader2, RefreshCw, Upload } from 'lucide-react';
+import { CheckCircle, Code2, Download, FileEdit, Loader2, RefreshCw, Upload } from 'lucide-react';
import { SettingsSectionHeader } from '../components';
import { CliStatusSection } from './CliStatusSection';
+import { ConfigEditorDialog } from './ConfigEditorDialog';
interface AdvancedSectionProps {
readonly saving: boolean;
@@ -30,6 +31,7 @@ export const AdvancedSection = ({
}: AdvancedSectionProps): React.JSX.Element => {
const isElectron = useMemo(() => isElectronMode(), []);
const [version, setVersion] = useState('');
+ const [configEditorOpen, setConfigEditorOpen] = useState(false);
const updateStatus = useStore((s) => s.updateStatus);
const availableVersion = useStore((s) => s.availableVersion);
const checkForUpdates = useStore((s) => s.checkForUpdates);
@@ -95,6 +97,17 @@ export const AdvancedSection = ({
+ setConfigEditorOpen(true)}
+ className="flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2.5 text-sm font-medium transition-all duration-150 hover:bg-white/5"
+ style={{
+ borderColor: 'var(--color-border)',
+ color: 'var(--color-text)',
+ }}
+ >
+
+ Edit Config
+
+
+ setConfigEditorOpen(false)}
+ onConfigSaved={() => {
+ // Config saved via editor — settings page will pick up changes on next render
+ }}
+ />
);
};
diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx
index 2e7647ca..aadc0c9c 100644
--- a/src/renderer/components/settings/sections/CliStatusSection.tsx
+++ b/src/renderer/components/settings/sections/CliStatusSection.tsx
@@ -27,6 +27,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
fetchCliStatus,
installCli,
isBusy,
+ cliStatusLoading,
} = useCliInstaller();
useEffect(() => {
@@ -129,14 +130,24 @@ export const CliStatusSection = (): React.JSX.Element | null => {
{cliStatus.installed && !cliStatus.updateAvailable && (
-
- Check for Updates
+ {cliStatusLoading ? (
+ <>
+
+ Checking...
+ >
+ ) : (
+ <>
+
+ Check for Updates
+ >
+ )}
)}
diff --git a/src/renderer/components/settings/sections/ConfigEditorDialog.tsx b/src/renderer/components/settings/sections/ConfigEditorDialog.tsx
new file mode 100644
index 00000000..02872b4e
--- /dev/null
+++ b/src/renderer/components/settings/sections/ConfigEditorDialog.tsx
@@ -0,0 +1,402 @@
+/**
+ * ConfigEditorDialog — inline JSON config editor powered by CodeMirror.
+ *
+ * Opens as a dialog, shows the full app config as formatted JSON.
+ * Auto-saves on changes with debounce. Shows validation errors for malformed JSON.
+ */
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
+import { json } from '@codemirror/lang-json';
+import { bracketMatching, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting } from '@codemirror/language';
+import { lintGutter, linter, type Diagnostic } from '@codemirror/lint';
+import { search, searchKeymap } from '@codemirror/search';
+import { EditorState } from '@codemirror/state';
+import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
+import {
+ EditorView,
+ highlightActiveLine,
+ highlightActiveLineGutter,
+ keymap,
+ lineNumbers,
+} from '@codemirror/view';
+import { api } from '@renderer/api';
+import { useStore } from '@renderer/store';
+import { baseEditorTheme } from '@renderer/utils/codemirrorTheme';
+import { AlertTriangle, Check, Loader2, X } from 'lucide-react';
+
+import type { AppConfig } from '@renderer/types/data';
+
+// =============================================================================
+// Constants
+// =============================================================================
+
+const SAVE_DEBOUNCE_MS = 800;
+
+// =============================================================================
+// JSON Linter
+// =============================================================================
+
+const jsonLinter = linter((view: EditorView) => {
+ const diagnostics: Diagnostic[] = [];
+ const text = view.state.doc.toString();
+ try {
+ JSON.parse(text);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ const match = e.message.match(/position (\d+)/);
+ const pos = match ? parseInt(match[1], 10) : 0;
+ const safePos = Math.min(pos, text.length);
+ diagnostics.push({
+ from: safePos,
+ to: Math.min(safePos + 1, text.length),
+ severity: 'error',
+ message: e.message,
+ });
+ }
+ }
+ return diagnostics;
+});
+
+// =============================================================================
+// Types
+// =============================================================================
+
+interface ConfigEditorDialogProps {
+ open: boolean;
+ onClose: () => void;
+ onConfigSaved: (config: AppConfig) => void;
+}
+
+type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
+
+// =============================================================================
+// Component
+// =============================================================================
+
+export const ConfigEditorDialog = ({
+ open,
+ onClose,
+ onConfigSaved,
+}: ConfigEditorDialogProps): React.JSX.Element | null => {
+ const editorRef = useRef(null);
+ const viewRef = useRef(null);
+ const saveTimerRef = useRef>();
+ const savedRevertTimerRef = useRef>();
+ const [saveStatus, setSaveStatus] = useState('idle');
+ const [jsonError, setJsonError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const initialConfigRef = useRef('');
+
+ const saveConfig = useCallback(
+ async (jsonText: string) => {
+ try {
+ const parsed = JSON.parse(jsonText) as AppConfig;
+ setJsonError(null);
+ setSaveStatus('saving');
+
+ // Save each section separately via existing API
+ if (parsed.general) {
+ await api.config.update('general', parsed.general);
+ }
+ if (parsed.notifications) {
+ await api.config.update('notifications', parsed.notifications);
+ }
+ if (parsed.display) {
+ await api.config.update('display', parsed.display);
+ }
+ if (parsed.sessions) {
+ await api.config.update('sessions', parsed.sessions);
+ }
+
+ // Re-fetch to get the canonical saved state
+ const fresh = await api.config.get();
+ onConfigSaved(fresh);
+ useStore.setState({ appConfig: fresh });
+ initialConfigRef.current = JSON.stringify(fresh, null, 2);
+
+ setSaveStatus('saved');
+ if (savedRevertTimerRef.current) clearTimeout(savedRevertTimerRef.current);
+ savedRevertTimerRef.current = setTimeout(() => setSaveStatus('idle'), 2000);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ setJsonError(e.message);
+ setSaveStatus('idle');
+ } else {
+ setSaveStatus('error');
+ setJsonError(e instanceof Error ? e.message : 'Failed to save config');
+ if (savedRevertTimerRef.current) clearTimeout(savedRevertTimerRef.current);
+ savedRevertTimerRef.current = setTimeout(() => {
+ setSaveStatus('idle');
+ setJsonError(null);
+ }, 4000);
+ }
+ }
+ },
+ [onConfigSaved]
+ );
+
+ const scheduleSave = useCallback(
+ (jsonText: string) => {
+ // Validate JSON before scheduling save
+ try {
+ JSON.parse(jsonText);
+ setJsonError(null);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ setJsonError(e.message);
+ }
+ return;
+ }
+
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
+ saveTimerRef.current = setTimeout(() => {
+ void saveConfig(jsonText);
+ }, SAVE_DEBOUNCE_MS);
+ },
+ [saveConfig]
+ );
+
+ // Initialize CodeMirror when dialog opens
+ useEffect(() => {
+ if (!open) return;
+
+ let destroyed = false;
+ setLoading(true);
+ setSaveStatus('idle');
+ setJsonError(null);
+
+ const init = async (): Promise => {
+ const config = await api.config.get();
+ if (destroyed) return;
+
+ const jsonText = JSON.stringify(config, null, 2);
+ initialConfigRef.current = jsonText;
+ setLoading(false);
+
+ // Wait for DOM render
+ requestAnimationFrame(() => {
+ if (destroyed || !editorRef.current) return;
+
+ // Clean up existing view
+ if (viewRef.current) {
+ viewRef.current.destroy();
+ viewRef.current = null;
+ }
+
+ const state = EditorState.create({
+ doc: jsonText,
+ extensions: [
+ lineNumbers(),
+ highlightActiveLineGutter(),
+ highlightActiveLine(),
+ history(),
+ foldGutter(),
+ indentOnInput(),
+ bracketMatching(),
+ json(),
+ syntaxHighlighting(oneDarkHighlightStyle),
+ jsonLinter,
+ lintGutter(),
+ search(),
+ keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap, ...searchKeymap]),
+ baseEditorTheme,
+ configEditorTheme,
+ EditorView.updateListener.of((update) => {
+ if (update.docChanged) {
+ const text = update.state.doc.toString();
+ scheduleSave(text);
+ }
+ }),
+ ],
+ });
+
+ const view = new EditorView({
+ state,
+ parent: editorRef.current,
+ });
+ viewRef.current = view;
+ });
+ };
+
+ void init();
+
+ return () => {
+ destroyed = true;
+ if (viewRef.current) {
+ viewRef.current.destroy();
+ viewRef.current = null;
+ }
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
+ if (savedRevertTimerRef.current) clearTimeout(savedRevertTimerRef.current);
+ };
+ }, [open, scheduleSave]);
+
+ // Escape key handler
+ useEffect(() => {
+ if (!open) return;
+ const handleKeyDown = (e: KeyboardEvent): void => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ onClose();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+ {
+ if (e.target === e.currentTarget) onClose();
+ }}
+ >
+
+ {/* Header */}
+
+
+
+ Edit Configuration
+
+
+
+
+
+
+
+
+ {/* Editor */}
+
+ {loading ? (
+
+
+ Loading config...
+
+ ) : (
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Changes auto-save after editing
+
+
+
+ Esc
+
+
+ to close
+
+
+
+
+
+ );
+};
+
+// =============================================================================
+// Save Status Badge
+// =============================================================================
+
+const SaveStatusBadge = ({
+ status,
+ error,
+}: {
+ status: SaveStatus;
+ error: string | null;
+}): React.JSX.Element | null => {
+ if (status === 'idle' && !error) return null;
+
+ if (error && status !== 'saving') {
+ return (
+
+
+ {status === 'error' ? 'Save failed' : 'Invalid JSON'}
+
+ );
+ }
+
+ if (status === 'saving') {
+ return (
+
+
+ Saving...
+
+ );
+ }
+
+ if (status === 'saved') {
+ return (
+
+
+ Saved
+
+ );
+ }
+
+ return null;
+};
+
+// =============================================================================
+// Editor Theme Override
+// =============================================================================
+
+const configEditorTheme = EditorView.theme({
+ '&': {
+ height: '100%',
+ maxHeight: 'calc(85vh - 100px)',
+ },
+ '.cm-scroller': {
+ overflow: 'auto',
+ padding: '8px 0',
+ },
+ '.cm-content': {
+ padding: '0 8px',
+ },
+ '.cm-gutters': {
+ paddingLeft: '4px',
+ },
+});
diff --git a/src/renderer/components/settings/sections/GeneralSection.tsx b/src/renderer/components/settings/sections/GeneralSection.tsx
index 16471af6..c78078fa 100644
--- a/src/renderer/components/settings/sections/GeneralSection.tsx
+++ b/src/renderer/components/settings/sections/GeneralSection.tsx
@@ -366,11 +366,16 @@ export const GeneralSection = ({
confirmLabel: 'Restart',
});
if (shouldRelaunch) {
- onGeneralToggle('useNativeTitleBar', v);
- // Small delay to let config persist before relaunch
- setTimeout(() => {
- void window.electronAPI?.windowControls?.relaunch();
- }, 200);
+ // Await config write before relaunch to avoid race condition on Windows
+ // (antivirus/NTFS can delay file writes beyond a fixed timeout)
+ try {
+ await api.config.update('general', { useNativeTitleBar: v });
+ } catch {
+ // If save fails, still try to toggle via the normal path
+ onGeneralToggle('useNativeTitleBar', v);
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ void window.electronAPI?.windowControls?.relaunch();
}
}}
disabled={saving}
@@ -503,7 +508,7 @@ export const GeneralSection = ({
{candidate.path}
{!candidate.hasProjectsDir && (
-
+
No projects directory detected
)}
diff --git a/src/renderer/components/team/CliLogsRichView.tsx b/src/renderer/components/team/CliLogsRichView.tsx
index 0b8cd8a5..971faecd 100644
--- a/src/renderer/components/team/CliLogsRichView.tsx
+++ b/src/renderer/components/team/CliLogsRichView.tsx
@@ -10,6 +10,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DisplayItemList } from '@renderer/components/chat/DisplayItemList';
+import { highlightQueryInText } from '@renderer/components/chat/searchHighlightUtils';
import { cn } from '@renderer/lib/utils';
import { parseStreamJsonToGroups } from '@renderer/utils/streamJsonParser';
import { Bot, ChevronRight } from 'lucide-react';
@@ -119,7 +120,11 @@ const StreamGroup = ({
/>
- {group.summary}
+ {searchQueryOverride && searchQueryOverride.trim().length > 0
+ ? highlightQueryInText(group.summary, searchQueryOverride, `${group.id}:group-summary`, {
+ forceAllActive: true,
+ })
+ : group.summary}
{isExpanded && (
diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx
index 64fd0789..59efa480 100644
--- a/src/renderer/components/team/ProvisioningProgressBlock.tsx
+++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx
@@ -185,7 +185,7 @@ export const ProvisioningProgressBlock = ({
{message}
@@ -201,9 +201,9 @@ export const ProvisioningProgressBlock = ({
variant="secondary"
className={cn(
'whitespace-nowrap px-2 py-0.5 text-[11px] font-normal',
- isDone && 'border-emerald-400/60 bg-emerald-500/10 text-emerald-200',
+ isDone && 'border-[var(--step-done-border)] bg-[var(--step-done-bg)] text-[var(--step-done-text)]',
isCurrent &&
- 'border-[var(--color-accent)]/70 bg-[var(--color-accent)]/15 text-[var(--color-text)]'
+ 'border-[var(--step-current-border)] bg-[var(--step-current-bg)] text-[var(--step-current-text)]'
)}
>
@@ -241,7 +241,7 @@ export const ProvisioningProgressBlock = ({
No output captured yet.
diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx
index 251538e0..64ab3f9c 100644
--- a/src/renderer/components/team/TeamDetailView.tsx
+++ b/src/renderer/components/team/TeamDetailView.tsx
@@ -1081,15 +1081,22 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
{!data.isAlive && !isTeamProvisioning ? (
-
-
-
+
+
+
Team is offline
setLaunchDialogOpen(true)}
>
@@ -1103,12 +1110,12 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
{data.warnings?.some((warning) => warning.toLowerCase().includes('kanban')) ? (
-
+
Failed to fully load kanban. Displaying safe data.
) : null}
{reviewActionError ? (
-
+
{reviewActionError}
) : null}
diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx
index 3b508855..7524b49d 100644
--- a/src/renderer/components/team/TeamListView.tsx
+++ b/src/renderer/components/team/TeamListView.tsx
@@ -556,10 +556,14 @@ export const TeamListView = (): React.JSX.Element => {
{
void fetchTeams();
}}
>
+ {teamsLoading ? (
+
+ ) : null}
Refresh
diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx
index c96ba6f6..20006dae 100644
--- a/src/renderer/components/team/TeamProvisioningBanner.tsx
+++ b/src/renderer/components/team/TeamProvisioningBanner.tsx
@@ -78,11 +78,11 @@ export const TeamProvisioningBanner = ({
return (
-
{progress.message}
+
{progress.message}
setDismissed(true)}
>
@@ -108,13 +108,13 @@ export const TeamProvisioningBanner = ({
if (isReady) {
return (
-
-
-
Team launched — process alive
+
+
+
Team launched — process alive
setDismissed(true)}
>
diff --git a/src/renderer/components/team/attachments/AttachmentPreviewList.tsx b/src/renderer/components/team/attachments/AttachmentPreviewList.tsx
index 164afb47..cb86dec8 100644
--- a/src/renderer/components/team/attachments/AttachmentPreviewList.tsx
+++ b/src/renderer/components/team/attachments/AttachmentPreviewList.tsx
@@ -38,9 +38,12 @@ export const AttachmentPreviewList = ({
) : null}
{disabled && disabledHint && attachments.length > 0 ? (
-
-
-
{disabledHint}
+
) : null}
{error ? (
diff --git a/src/renderer/components/team/dialogs/CreateTaskDialog.tsx b/src/renderer/components/team/dialogs/CreateTaskDialog.tsx
index d650bef7..6f0f0fc2 100644
--- a/src/renderer/components/team/dialogs/CreateTaskDialog.tsx
+++ b/src/renderer/components/team/dialogs/CreateTaskDialog.tsx
@@ -241,9 +241,16 @@ export const CreateTaskDialog = ({
{!isTeamAlive ? (
-
-
-
+
+
+
Team is offline. The task will be added to TODO — launch the
team to start execution.
diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
index 41768955..f87509bc 100644
--- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
+++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
@@ -588,25 +588,32 @@ export const CreateTeamDialog = ({
{conflictingTeam && !conflictDismissed ? (
-
+
-
+
-
+
Another team “{conflictingTeam.displayName}” is already running for
this working directory
-
+
Running two teams in the same directory is risky — they may conflict editing the
same files. Consider using a different directory or a git worktree for isolation.
-
+
Working directory: {effectiveCwd}
setConflictDismissed(true)}
>
@@ -629,7 +636,7 @@ export const CreateTeamDialog = ({
{prepareWarnings.length > 0 ? (
{prepareWarnings.map((warning) => (
-
+
{warning}
))}
@@ -645,7 +652,14 @@ export const CreateTeamDialog = ({
) : null}
{!canCreate ? (
-
+
Available only in local Electron mode.
) : null}
diff --git a/src/renderer/components/team/dialogs/ExtendedContextCheckbox.tsx b/src/renderer/components/team/dialogs/ExtendedContextCheckbox.tsx
index fbfca008..7a93ac04 100644
--- a/src/renderer/components/team/dialogs/ExtendedContextCheckbox.tsx
+++ b/src/renderer/components/team/dialogs/ExtendedContextCheckbox.tsx
@@ -36,10 +36,17 @@ export const ExtendedContextCheckbox: React.FC
= (
{checked && (
-
+
-
-
+
+
Beyond 200K tokens, premium pricing applies: 2x input cost, 1.5x output cost. For
subscribers, extra usage is billed separately.
@@ -48,7 +55,7 @@ export const ExtendedContextCheckbox: React.FC = (
Requires API tier 4+ or extra usage enabled.{' '}
window.electronAPI.openExternal(
'https://platform.claude.com/docs/en/build-with-claude/context-windows#1m-token-context-window'
diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
index e495abd1..3a296265 100644
--- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
+++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
@@ -314,25 +314,32 @@ export const LaunchTeamDialog = ({
{conflictingTeam && !conflictDismissed ? (
-
+
-
+
-
+
Another team “{conflictingTeam.displayName}” is already running for
this working directory
-
+
Running two teams in the same directory is risky — they may conflict editing the
same files. Consider using a different directory or a git worktree for isolation.
-
+
Working directory: {effectiveCwd}
setConflictDismissed(true)}
>
@@ -355,7 +362,7 @@ export const LaunchTeamDialog = ({
{prepareWarnings.length > 0 ? (
{prepareWarnings.map((warning) => (
-
+
{warning}
))}
@@ -438,10 +445,17 @@ export const LaunchTeamDialog = ({
{clearContext && (
-
+
-
-
+
+
The team lead will start a new session without resuming previous context. All
accumulated session memory and conversation history will not be available.
diff --git a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx
index 70c54209..62759c8d 100644
--- a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx
+++ b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx
@@ -137,7 +137,7 @@ export const ProjectPathSelector = ({
) : null}
{projectsError ?
{projectsError}
: null}
{!projectsLoading && projects.length === 0 ? (
-
No projects found, switch to custom path.
+
No projects found, switch to custom path.
) : null}
) : (
diff --git a/src/renderer/components/team/editor/ProjectEditorOverlay.tsx b/src/renderer/components/team/editor/ProjectEditorOverlay.tsx
index c2dd57d8..b823b767 100644
--- a/src/renderer/components/team/editor/ProjectEditorOverlay.tsx
+++ b/src/renderer/components/team/editor/ProjectEditorOverlay.tsx
@@ -678,7 +678,14 @@ export const ProjectEditorOverlay = ({
{/* Draft recovery banner */}
{draftRecoveredFile && activeTabId === draftRecoveredFile && (
-
+
Recovered unsaved changes from a previous session.
Team offline
+ Team offline
) : null}
diff --git a/src/renderer/index.css b/src/renderer/index.css
index 60d9387e..1e18c1ca 100644
--- a/src/renderer/index.css
+++ b/src/renderer/index.css
@@ -198,6 +198,20 @@
--skeleton-base: #1a1c28;
--skeleton-base-light: #23252f;
--skeleton-base-dim: rgba(26, 28, 40, 0.6);
+
+ /* Provisioning step badges */
+ --step-done-bg: rgba(16, 185, 129, 0.1);
+ --step-done-border: rgba(52, 211, 153, 0.6);
+ --step-done-text: #6ee7b7;
+ --step-current-bg: rgba(99, 102, 241, 0.15);
+ --step-current-border: rgba(129, 140, 248, 0.7);
+ --step-current-text: #f1f5f9;
+ --step-error-text: #fca5a5;
+ --step-error-text-dim: rgba(252, 165, 165, 0.8);
+ --step-success-text: #6ee7b7;
+ --step-warning-text: #fde68a;
+ --step-warning-border: rgba(245, 158, 11, 0.4);
+ --step-warning-bg: rgba(245, 158, 11, 0.1);
}
/* File icon glow — halo so dark icons stay visible on dark backgrounds */
@@ -400,6 +414,20 @@
--skeleton-base: #d6d8de;
--skeleton-base-light: #cdd0d7;
--skeleton-base-dim: rgba(205, 208, 215, 0.6);
+
+ /* Provisioning step badges — dark enough for light backgrounds */
+ --step-done-bg: rgba(16, 185, 129, 0.12);
+ --step-done-border: rgba(5, 150, 105, 0.5);
+ --step-done-text: #047857;
+ --step-current-bg: rgba(79, 70, 229, 0.1);
+ --step-current-border: rgba(79, 70, 229, 0.5);
+ --step-current-text: #1c1b19;
+ --step-error-text: #dc2626;
+ --step-error-text-dim: rgba(220, 38, 38, 0.7);
+ --step-success-text: #047857;
+ --step-warning-text: #b45309;
+ --step-warning-border: rgba(180, 83, 9, 0.4);
+ --step-warning-bg: rgba(245, 158, 11, 0.1);
}
/* rehype-highlight (highlight.js) — map hljs classes to app theme variables */
diff --git a/src/renderer/store/slices/updateSlice.ts b/src/renderer/store/slices/updateSlice.ts
index aa0852c8..5490acc6 100644
--- a/src/renderer/store/slices/updateSlice.ts
+++ b/src/renderer/store/slices/updateSlice.ts
@@ -57,6 +57,7 @@ export const createUpdateSlice: StateCreator
= (s
set({ updateStatus: 'checking', updateError: null });
api.updater.check().catch((error) => {
logger.error('Failed to check for updates:', error);
+ set({ updateStatus: 'error', updateError: error instanceof Error ? error.message : 'Check failed' });
});
},