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.
This commit is contained in:
iliya 2026-03-05 18:57:07 +02:00
parent c963c8a409
commit 80147c9900
35 changed files with 757 additions and 87 deletions

View file

@ -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",

View file

@ -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

View file

@ -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) });
}
}

View file

@ -161,6 +161,8 @@ interface ProvisioningRun {
* request triggered by the UI. We suppress any leaduser 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) {

View file

@ -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)}

View file

@ -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") */

View file

@ -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<LinkedToolItemProps> = ({
linkedTool,
onClick,
isExpanded,
searchQueryOverride,
isHighlighted,
highlightColor,
notificationDotColor,
@ -66,6 +70,12 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
}) => {
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<HTMLDivElement>(null);
// Combined ref callback - handles both internal ref and external registration
@ -155,7 +165,7 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
/>
}
label={linkedTool.name}
summary={summary}
summary={summaryNode}
tokenCount={getToolContextTokens(linkedTool)}
status={status}
durationMs={linkedTool.durationMs}

View file

@ -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<TextItemProps> = ({
}) => {
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<TextItemProps> = ({
<BaseItem
icon={<MessageSquare className="size-4" />}
label="Output"
summary={truncatedPreview}
summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}

View file

@ -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<ThinkingItemProps> = ({
}) => {
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<ThinkingItemProps> = ({
<BaseItem
icon={<Brain className="size-4" />}
label="Thinking"
summary={truncatedPreview}
summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}

View file

@ -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.)

View file

@ -600,6 +600,10 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
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)

View file

@ -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 => (
<div
className={`flex items-start gap-2 rounded-md border px-3 py-2 text-xs ${className}`}
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
{icon ?? <AlertTriangle size={14} className="mt-0.5 shrink-0" />}
<div className="min-w-0 flex-1">{children}</div>
</div>
);

View file

@ -44,9 +44,11 @@ export const SidebarHeader = (): React.JSX.Element => {
} as React.CSSProperties
}
>
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<AppLogo size={22} className="shrink-0" />
</div>
{isMacElectron && (
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<AppLogo size={22} className="shrink-0" />
</div>
)}
<div className="flex-1" />
<button
onClick={toggleSidebar}

View file

@ -7,6 +7,7 @@ import { useCallback, useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useStore } from '@renderer/store';
import {
Activity,
@ -66,6 +67,13 @@ export const SortableTab = ({
)
);
const teamColor = useStore((s) => {
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];

View file

@ -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. */}
<div
className="flex-1 self-stretch"
className="min-w-[48px] flex-1 self-stretch"
style={
{
WebkitAppRegion: isElectronMode() && isLeftmostPane ? 'drag' : undefined,

View file

@ -63,7 +63,14 @@ export const TriggerPreview = ({
{/* Truncation warning - only shown when timeout or count limit hit */}
{previewResult.truncated && (
<div className="flex items-center gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-400">
<div
className="flex items-center gap-2 rounded border px-3 py-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<AlertTriangle className="size-4 shrink-0" />
<span>
Search stopped early (timeout or count limit). Actual matches may be higher.

View file

@ -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<string>('');
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 = ({
<div>
<SettingsSectionHeader title="Configuration" />
<div className="space-y-2 py-2">
<button
onClick={() => 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)',
}}
>
<FileEdit className="size-4" />
Edit Config
</button>
<button
onClick={onResetToDefaults}
disabled={saving}
@ -195,6 +208,14 @@ export const AdvancedSection = ({
</p>
</div>
</div>
<ConfigEditorDialog
open={configEditorOpen}
onClose={() => setConfigEditorOpen(false)}
onConfigSaved={() => {
// Config saved via editor — settings page will pick up changes on next render
}}
/>
</div>
);
};

View file

@ -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 && (
<button
onClick={handleRefresh}
className="flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors hover:bg-white/5"
disabled={cliStatusLoading}
className="flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors hover:bg-white/5 disabled:opacity-50"
style={{
borderColor: 'var(--color-border)',
color: 'var(--color-text-secondary)',
}}
>
<RefreshCw className="size-3.5" />
Check for Updates
{cliStatusLoading ? (
<>
<Loader2 className="size-3.5 animate-spin" />
Checking...
</>
) : (
<>
<RefreshCw className="size-3.5" />
Check for Updates
</>
)}
</button>
)}
</div>

View file

@ -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<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const saveTimerRef = useRef<ReturnType<typeof setTimeout>>();
const savedRevertTimerRef = useRef<ReturnType<typeof setTimeout>>();
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
const [jsonError, setJsonError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const initialConfigRef = useRef<string>('');
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<void> => {
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 (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.6)' }}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
className="flex max-h-[85vh] w-full max-w-3xl flex-col overflow-hidden rounded-xl border shadow-2xl"
style={{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-border-emphasis)',
}}
>
{/* Header */}
<div
className="flex items-center justify-between border-b px-4 py-3"
style={{ borderColor: 'var(--color-border)' }}
>
<div className="flex items-center gap-3">
<h2 className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Edit Configuration
</h2>
<SaveStatusBadge status={saveStatus} error={jsonError} />
</div>
<button
onClick={onClose}
className="rounded-md p-1 transition-colors hover:bg-white/10"
style={{ color: 'var(--color-text-muted)' }}
>
<X className="size-4" />
</button>
</div>
{/* Editor */}
<div className="relative min-h-0 flex-1">
{loading ? (
<div
className="flex h-96 items-center justify-center gap-2 text-sm"
style={{ color: 'var(--color-text-muted)' }}
>
<Loader2 className="size-4 animate-spin" />
Loading config...
</div>
) : (
<div ref={editorRef} className="config-editor-container h-full min-h-[400px]" />
)}
</div>
{/* Footer */}
<div
className="flex items-center justify-between border-t px-4 py-2.5"
style={{ borderColor: 'var(--color-border)' }}
>
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
Changes auto-save after editing
</p>
<div className="flex items-center gap-2">
<kbd
className="rounded px-1.5 py-0.5 text-[10px]"
style={{
backgroundColor: 'var(--color-surface-raised)',
color: 'var(--color-text-muted)',
border: '1px solid var(--color-border)',
}}
>
Esc
</kbd>
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
to close
</span>
</div>
</div>
</div>
</div>
);
};
// =============================================================================
// 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 (
<span
className="flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px]"
style={{ backgroundColor: 'rgba(248, 113, 113, 0.15)', color: '#f87171' }}
title={error}
>
<AlertTriangle className="size-3" />
{status === 'error' ? 'Save failed' : 'Invalid JSON'}
</span>
);
}
if (status === 'saving') {
return (
<span
className="flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px]"
style={{ backgroundColor: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}
>
<Loader2 className="size-3 animate-spin" />
Saving...
</span>
);
}
if (status === 'saved') {
return (
<span
className="flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px]"
style={{ backgroundColor: 'rgba(74, 222, 128, 0.15)', color: '#4ade80' }}
>
<Check className="size-3" />
Saved
</span>
);
}
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',
},
});

View file

@ -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}
</p>
{!candidate.hasProjectsDir && (
<p className="text-[11px] text-amber-400">
<p className="text-[11px]" style={{ color: 'var(--warning-text)' }}>
No projects directory detected
</p>
)}

View file

@ -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 = ({
/>
<Bot size={13} className="shrink-0 text-[var(--color-text-muted)]" />
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
{group.summary}
{searchQueryOverride && searchQueryOverride.trim().length > 0
? highlightQueryInText(group.summary, searchQueryOverride, `${group.id}:group-summary`, {
forceAllActive: true,
})
: group.summary}
</span>
</button>
{isExpanded && (

View file

@ -185,7 +185,7 @@ export const ProvisioningProgressBlock = ({
<p
className={cn(
'mt-1.5 text-xs',
isError ? 'text-red-200' : 'text-[var(--color-text-muted)]'
isError ? 'text-[var(--step-error-text)]' : 'text-[var(--color-text-muted)]'
)}
>
{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)]'
)}
>
<span className="mr-1 inline-flex size-4 items-center justify-center rounded-full border border-current text-[10px]">
@ -241,7 +241,7 @@ export const ProvisioningProgressBlock = ({
<p
className={cn(
'text-[11px]',
isError ? 'text-red-200/80' : 'text-[var(--color-text-muted)]'
isError ? 'text-[var(--step-error-text-dim)]' : 'text-[var(--color-text-muted)]'
)}
>
No output captured yet.

View file

@ -1081,15 +1081,22 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
</div>
{!data.isAlive && !isTeamProvisioning ? (
<div className="mb-3 flex items-center justify-between gap-3 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2">
<span className="flex items-center gap-1.5 text-xs text-amber-200">
<AlertTriangle size={14} className="shrink-0 text-amber-400" />
<div
className="mb-3 flex items-center justify-between gap-3 rounded-md border px-3 py-2"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<span className="flex items-center gap-1.5 text-xs">
<AlertTriangle size={14} className="shrink-0" />
Team is offline
</span>
<Button
variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1 px-2 text-xs text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
className="h-7 shrink-0 gap-1 px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
onClick={() => setLaunchDialogOpen(true)}
>
<Play size={12} />
@ -1103,12 +1110,12 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
</div>
{data.warnings?.some((warning) => warning.toLowerCase().includes('kanban')) ? (
<div className="mb-3 rounded-md border border-yellow-500/40 bg-yellow-500/10 px-3 py-2 text-xs text-yellow-200">
<div className="mb-3 rounded-md border border-[var(--step-warning-border)] bg-[var(--step-warning-bg)] px-3 py-2 text-xs text-[var(--step-warning-text)]">
Failed to fully load kanban. Displaying safe data.
</div>
) : null}
{reviewActionError ? (
<div className="mb-3 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-red-200">
<div className="mb-3 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-[var(--step-error-text)]">
{reviewActionError}
</div>
) : null}

View file

@ -556,10 +556,14 @@ export const TeamListView = (): React.JSX.Element => {
<Button
variant="outline"
size="sm"
disabled={teamsLoading}
onClick={() => {
void fetchTeams();
}}
>
{teamsLoading ? (
<RotateCcw className="size-3.5 animate-spin" />
) : null}
Refresh
</Button>
</div>

View file

@ -78,11 +78,11 @@ export const TeamProvisioningBanner = ({
return (
<div className="mb-3">
<div className="mb-2 flex items-center gap-2 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2">
<p className="flex-1 text-xs text-red-200">{progress.message}</p>
<p className="flex-1 text-xs text-[var(--step-error-text)]">{progress.message}</p>
<Button
variant="outline"
size="sm"
className="h-6 shrink-0 border-red-500/40 px-2 text-xs text-red-300 hover:bg-red-500/10 hover:text-red-200"
className="h-6 shrink-0 border-red-500/40 px-2 text-xs text-[var(--step-error-text)] hover:bg-red-500/10"
onClick={() => setDismissed(true)}
>
<X size={12} />
@ -108,13 +108,13 @@ export const TeamProvisioningBanner = ({
if (isReady) {
return (
<div className="mb-3">
<div className="mb-2 flex items-center gap-2 rounded-md border border-emerald-500/40 bg-emerald-500/10 px-3 py-2">
<CheckCircle2 size={14} className="shrink-0 text-emerald-400" />
<p className="flex-1 text-xs text-emerald-200">Team launched process alive</p>
<div className="mb-2 flex items-center gap-2 rounded-md border border-[var(--step-done-border)] bg-[var(--step-done-bg)] px-3 py-2">
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
<p className="flex-1 text-xs text-[var(--step-success-text)]">Team launched process alive</p>
<Button
variant="outline"
size="sm"
className="h-6 shrink-0 border-emerald-500/40 px-2 text-xs text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200"
className="h-6 shrink-0 border-[var(--step-done-border)] px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
onClick={() => setDismissed(true)}
>
<X size={12} />

View file

@ -38,9 +38,12 @@ export const AttachmentPreviewList = ({
</div>
) : null}
{disabled && disabledHint && attachments.length > 0 ? (
<div className="flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2.5 py-1.5">
<AlertCircle size={13} className="shrink-0 text-amber-400" />
<p className="text-[11px] text-amber-400">{disabledHint}</p>
<div
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5"
style={{ backgroundColor: 'var(--warning-bg)', color: 'var(--warning-text)' }}
>
<AlertCircle size={13} className="shrink-0" />
<p className="text-[11px]">{disabledHint}</p>
</div>
) : null}
{error ? (

View file

@ -241,9 +241,16 @@ export const CreateTaskDialog = ({
</DialogHeader>
{!isTeamAlive ? (
<div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2">
<AlertTriangle size={14} className="mt-0.5 shrink-0 text-amber-400" />
<p className="text-xs leading-relaxed text-amber-300">
<div
className="flex items-start gap-2 rounded-md border px-3 py-2"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
<p className="text-xs leading-relaxed">
Team is offline. The task will be added to <strong>TODO</strong> &mdash; launch the
team to start execution.
</p>

View file

@ -588,25 +588,32 @@ export const CreateTeamDialog = ({
</DialogHeader>
{conflictingTeam && !conflictDismissed ? (
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-xs">
<div
className="rounded-md border p-3 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1 space-y-1">
<p className="font-medium text-amber-300">
<p className="font-medium">
Another team &ldquo;{conflictingTeam.displayName}&rdquo; is already running for
this working directory
</p>
<p className="text-amber-300/80">
<p className="opacity-80">
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.
</p>
<p className="text-[11px] text-amber-300/70">
<p className="text-[11px] opacity-70">
Working directory: <span className="font-mono">{effectiveCwd}</span>
</p>
</div>
<button
type="button"
className="shrink-0 rounded p-0.5 text-amber-400/60 transition-colors hover:text-amber-300"
className="shrink-0 rounded p-0.5 opacity-60 transition-colors hover:opacity-100"
onClick={() => setConflictDismissed(true)}
>
<X className="size-3.5" />
@ -629,7 +636,7 @@ export const CreateTeamDialog = ({
{prepareWarnings.length > 0 ? (
<div className="space-y-0.5">
{prepareWarnings.map((warning) => (
<p key={warning} className="text-[11px] text-amber-300">
<p key={warning} className="text-[11px]" style={{ color: 'var(--warning-text)' }}>
{warning}
</p>
))}
@ -645,7 +652,14 @@ export const CreateTeamDialog = ({
) : null}
{!canCreate ? (
<p className="rounded border border-amber-500/40 bg-amber-500/10 p-2 text-xs text-amber-300">
<p
className="rounded border p-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
Available only in local Electron mode.
</p>
) : null}

View file

@ -36,10 +36,17 @@ export const ExtendedContextCheckbox: React.FC<ExtendedContextCheckboxProps> = (
</Label>
</div>
{checked && (
<div className="mt-1.5 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs">
<div
className="mt-1.5 rounded-md border px-3 py-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0 text-amber-400" />
<div className="space-y-1 text-amber-300/90">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<div className="space-y-1">
<p>
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<ExtendedContextCheckboxProps> = (
Requires API tier 4+ or extra usage enabled.{' '}
<button
type="button"
className="underline underline-offset-2 hover:text-amber-200"
className="underline underline-offset-2 hover:opacity-80"
onClick={() =>
window.electronAPI.openExternal(
'https://platform.claude.com/docs/en/build-with-claude/context-windows#1m-token-context-window'

View file

@ -314,25 +314,32 @@ export const LaunchTeamDialog = ({
</DialogHeader>
{conflictingTeam && !conflictDismissed ? (
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-xs">
<div
className="rounded-md border p-3 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1 space-y-1">
<p className="font-medium text-amber-300">
<p className="font-medium">
Another team &ldquo;{conflictingTeam.displayName}&rdquo; is already running for
this working directory
</p>
<p className="text-amber-300/80">
<p className="opacity-80">
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.
</p>
<p className="text-[11px] text-amber-300/70">
<p className="text-[11px] opacity-70">
Working directory: <span className="font-mono">{effectiveCwd}</span>
</p>
</div>
<button
type="button"
className="shrink-0 rounded p-0.5 text-amber-400/60 transition-colors hover:text-amber-300"
className="shrink-0 rounded p-0.5 opacity-60 transition-colors hover:opacity-100"
onClick={() => setConflictDismissed(true)}
>
<X className="size-3.5" />
@ -355,7 +362,7 @@ export const LaunchTeamDialog = ({
{prepareWarnings.length > 0 ? (
<div className="space-y-0.5">
{prepareWarnings.map((warning) => (
<p key={warning} className="text-[11px] text-amber-300">
<p key={warning} className="text-[11px]" style={{ color: 'var(--warning-text)' }}>
{warning}
</p>
))}
@ -438,10 +445,17 @@ export const LaunchTeamDialog = ({
</Label>
</div>
{clearContext && (
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs">
<div
className="rounded-md border px-3 py-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0 text-amber-400" />
<p className="text-amber-300/90">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<p>
The team lead will start a new session without resuming previous context. All
accumulated session memory and conversation history will not be available.
</p>

View file

@ -137,7 +137,7 @@ export const ProjectPathSelector = ({
) : null}
{projectsError ? <p className="text-[11px] text-red-300">{projectsError}</p> : null}
{!projectsLoading && projects.length === 0 ? (
<p className="text-[11px] text-amber-300">No projects found, switch to custom path.</p>
<p className="text-[11px]" style={{ color: 'var(--warning-text)' }}>No projects found, switch to custom path.</p>
) : null}
</div>
) : (

View file

@ -678,7 +678,14 @@ export const ProjectEditorOverlay = ({
{/* Draft recovery banner */}
{draftRecoveredFile && activeTabId === draftRecoveredFile && (
<div className="flex shrink-0 items-center gap-2 border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5 text-xs text-amber-300">
<div
className="flex shrink-0 items-center gap-2 border-b px-3 py-1.5 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<RotateCcw className="size-3.5 shrink-0" />
<span>Recovered unsaved changes from a previous session.</span>
<Button

View file

@ -392,7 +392,7 @@ export const MessageComposer = ({
) : null}
{!isTeamAlive ? (
<span className="ml-auto text-[10px] text-amber-400">Team offline</span>
<span className="ml-auto text-[10px]" style={{ color: 'var(--warning-text)' }}>Team offline</span>
) : null}
</div>

View file

@ -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 */

View file

@ -57,6 +57,7 @@ export const createUpdateSlice: StateCreator<AppState, [], [], UpdateSlice> = (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' });
});
},