Skip to content

Commit 8a1fd4c

Browse files
hameltomorclaude
andcommitted
feat: upstream sync — sidebar status pills, streaming fix, and desktop improvements
Port high-value changes from upstream t3code: - Add "Awaiting Input" (indigo) and "Plan Ready" (violet) sidebar status pills with 6-level priority chain, extracted into Sidebar.logic.ts with full unit test coverage (15 tests) - Fix streamed assistant text duplication by guarding fallback completion text when message already has content (upstream PR pingdotgg#465) - Add dismiss button to thread error banner (upstream PR pingdotgg#588) - Add spellcheck suggestions and standard edit actions to desktop right-click context menu (upstream PR pingdotgg#500) - Use filesystem-friendly userData directory name for Electron with legacy path fallback (upstream PR pingdotgg#607) - Fix plan expand/collapse button triggering scroll anchoring Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5e64bef commit 8a1fd4c

6 files changed

Lines changed: 456 additions & 86 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const ROOT_DIR = Path.resolve(__dirname, "../../..");
5050
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
5151
const APP_DISPLAY_NAME = isDevelopment ? "XBE Code (Dev)" : "XBE Code";
5252
const APP_USER_MODEL_ID = "com.xbetools.xbecode";
53+
const USER_DATA_DIR_NAME = isDevelopment ? "xbecode-dev" : "xbecode";
54+
const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "XBE Code (Dev)" : "XBE Code";
5355
const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;
5456
const COMMIT_HASH_DISPLAY_LENGTH = 12;
5557
const LOG_DIR = Path.join(STATE_DIR, "logs");
@@ -574,6 +576,34 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
574576
return resolveResourcePath(`icon.${ext}`);
575577
}
576578

579+
/**
580+
* Resolve the Electron userData directory path.
581+
*
582+
* Electron derives the default userData path from `productName` in
583+
* package.json, which can produce directories with spaces and
584+
* parentheses (e.g. `~/.config/XBE Code` on Linux). This is
585+
* unfriendly for shell usage and violates Linux naming conventions.
586+
*
587+
* We override it to a clean lowercase name (`xbecode`). If the legacy
588+
* directory already exists we keep using it so existing users don't
589+
* lose their Chromium profile data (localStorage, cookies, sessions).
590+
*/
591+
function resolveUserDataPath(): string {
592+
const appDataBase =
593+
process.platform === "win32"
594+
? process.env.APPDATA || Path.join(OS.homedir(), "AppData", "Roaming")
595+
: process.platform === "darwin"
596+
? Path.join(OS.homedir(), "Library", "Application Support")
597+
: process.env.XDG_CONFIG_HOME || Path.join(OS.homedir(), ".config");
598+
599+
const legacyPath = Path.join(appDataBase, LEGACY_USER_DATA_DIR_NAME);
600+
if (FS.existsSync(legacyPath)) {
601+
return legacyPath;
602+
}
603+
604+
return Path.join(appDataBase, USER_DATA_DIR_NAME);
605+
}
606+
577607
function configureAppIdentity(): void {
578608
app.setName(APP_DISPLAY_NAME);
579609
const commitHash = resolveAboutCommitHash();
@@ -1114,6 +1144,34 @@ function createWindow(): BrowserWindow {
11141144
},
11151145
});
11161146

1147+
window.webContents.on("context-menu", (event, params) => {
1148+
event.preventDefault();
1149+
1150+
const menuTemplate: MenuItemConstructorOptions[] = [];
1151+
1152+
if (params.misspelledWord) {
1153+
for (const suggestion of params.dictionarySuggestions.slice(0, 5)) {
1154+
menuTemplate.push({
1155+
label: suggestion,
1156+
click: () => window.webContents.replaceMisspelling(suggestion),
1157+
});
1158+
}
1159+
if (params.dictionarySuggestions.length === 0) {
1160+
menuTemplate.push({ label: "No suggestions", enabled: false });
1161+
}
1162+
menuTemplate.push({ type: "separator" });
1163+
}
1164+
1165+
menuTemplate.push(
1166+
{ role: "cut", enabled: params.editFlags.canCut },
1167+
{ role: "copy", enabled: params.editFlags.canCopy },
1168+
{ role: "paste", enabled: params.editFlags.canPaste },
1169+
{ role: "selectAll", enabled: params.editFlags.canSelectAll },
1170+
);
1171+
1172+
Menu.buildFromTemplate(menuTemplate).popup({ window });
1173+
});
1174+
11171175
window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
11181176
window.on("page-title-updated", (event) => {
11191177
event.preventDefault();
@@ -1143,6 +1201,11 @@ function createWindow(): BrowserWindow {
11431201
return window;
11441202
}
11451203

1204+
// Override Electron's userData path before the `ready` event so that
1205+
// Chromium session data uses a filesystem-friendly directory name.
1206+
// Must be called synchronously at the top level — before `app.whenReady()`.
1207+
app.setPath("userData", resolveUserDataPath());
1208+
11461209
configureAppIdentity();
11471210

11481211
async function bootstrap(): Promise<void> {

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,10 @@ const make = Effect.gen(function* () {
989989
if (assistantCompletion) {
990990
const assistantMessageId = assistantCompletion.messageId;
991991
const turnId = toTurnId(event.turnId);
992+
const existingAssistantMessage = thread.messages.find((entry) => entry.id === assistantMessageId);
993+
const shouldApplyFallbackCompletionText =
994+
!existingAssistantMessage ||
995+
existingAssistantMessage.text.length === 0;
992996
if (turnId) {
993997
yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
994998
}
@@ -1001,7 +1005,7 @@ const make = Effect.gen(function* () {
10011005
createdAt: now,
10021006
commandTag: "assistant-complete",
10031007
finalDeltaCommandTag: "assistant-delta-finalize",
1004-
...(assistantCompletion.fallbackText !== undefined
1008+
...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText
10051009
? { fallbackText: assistantCompletion.fallbackText }
10061010
: {}),
10071011
});

apps/web/src/components/ChatView.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ import {
131131
} from "../keybindings";
132132
import ChatMarkdown from "./ChatMarkdown";
133133
import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
134-
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
134+
import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
135135
import {
136136
BotIcon,
137137
ChevronDownIcon,
@@ -1780,6 +1780,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
17801780
"button, summary, [role='button'], [data-scroll-anchor-target]",
17811781
);
17821782
if (!trigger || !scrollContainer.contains(trigger)) return;
1783+
if (trigger.closest("[data-scroll-anchor-ignore]")) return;
17831784

17841785
pendingInteractionAnchorRef.current = {
17851786
element: trigger,
@@ -3578,7 +3579,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
35783579

35793580
{/* Error banner */}
35803581
<ProviderHealthBanner status={activeProviderStatus} />
3581-
<ThreadErrorBanner error={activeThread.error} />
3582+
<ThreadErrorBanner
3583+
error={activeThread.error}
3584+
onDismiss={() => setThreadError(activeThread.id, null)}
3585+
/>
35823586
<PlanModePanel activePlan={activePlan} />
35833587

35843588
{/* Messages */}
@@ -4218,31 +4222,31 @@ const ChatHeader = memo(function ChatHeader({
42184222
const gitActionsRepoCwd = isMultiRepo ? (selectedRepoCwd ?? workspaceRepos[0]?.path ?? null) : gitCwd;
42194223

42204224
return (
4221-
<div className="flex min-w-0 flex-1 flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-2">
4222-
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
4225+
<div className="flex min-w-0 flex-1 flex-row items-center gap-2 sm:gap-2">
4226+
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden sm:flex-1 sm:gap-3">
42234227
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
42244228
<h2
4225-
className="min-w-0 shrink truncate text-sm font-medium text-foreground"
4229+
className="max-w-12 sm:max-w-none min-w-0 shrink truncate text-sm font-medium text-foreground"
42264230
title={activeThreadTitle}
42274231
>
42284232
{activeThreadTitle}
42294233
</h2>
42304234
{activeProjectName && truncatedProjectName && (
42314235
<Badge
42324236
variant="outline"
4233-
className="max-w-32 min-w-0 shrink-0 justify-start overflow-hidden"
4237+
className="max-w-18 sm:max-w-32 min-w-0 shrink-0 justify-start overflow-hidden"
42344238
title={activeProjectName}
42354239
>
42364240
<span className="block truncate">{truncatedProjectName}</span>
42374241
</Badge>
42384242
)}
42394243
{activeProjectName && !isGitRepo && !isMultiRepo && (
4240-
<Badge variant="outline" className="shrink-0 text-[10px] text-amber-700">
4244+
<Badge variant="outline" className="hidden sm:inline-flex shrink-0 text-[10px] text-amber-700">
42414245
No Git
42424246
</Badge>
42434247
)}
42444248
</div>
4245-
<div className="@container/header-actions flex w-full min-w-0 items-center gap-2 @sm/header-actions:gap-3 sm:w-auto sm:flex-1 sm:justify-end">
4249+
<div className="@container/header-actions flex min-w-0 flex-1 items-center justify-end gap-1 sm:gap-2 @sm/header-actions:gap-3">
42464250
{activeProjectScripts && (
42474251
<div className="hidden sm:contents">
42484252
<ProjectScriptsControl
@@ -4265,7 +4269,7 @@ const ChatHeader = memo(function ChatHeader({
42654269
</div>
42664270
)}
42674271
{activeProjectName && isMultiRepo && activeProjectId && workspaceReposQueryCwd && (
4268-
<div className="min-w-0 flex-1 sm:flex-initial [&_button]:sm:max-w-48 [&_button]:max-w-none [&_button]:w-full">
4272+
<div className="min-w-0 [&_button]:max-w-24 [&_button]:sm:max-w-48">
42694273
<RepoSwitcher
42704274
projectId={activeProjectId}
42714275
workspaceRoot={workspaceReposQueryCwd}
@@ -4300,15 +4304,21 @@ const ChatHeader = memo(function ChatHeader({
43004304
: "Toggle diff panel"}
43014305
</TooltipPopup>
43024306
</Tooltip>
4303-
<div className="shrink-0 md:hidden">
4307+
<div className="shrink-0">
43044308
<NotificationBell />
43054309
</div>
43064310
</div>
43074311
</div>
43084312
);
43094313
});
43104314

4311-
const ThreadErrorBanner = memo(function ThreadErrorBanner({ error }: { error: string | null }) {
4315+
const ThreadErrorBanner = memo(function ThreadErrorBanner({
4316+
error,
4317+
onDismiss,
4318+
}: {
4319+
error: string | null;
4320+
onDismiss?: () => void;
4321+
}) {
43124322
if (!error) return null;
43134323
return (
43144324
<div className="pt-3 mx-auto max-w-3xl">
@@ -4317,6 +4327,18 @@ const ThreadErrorBanner = memo(function ThreadErrorBanner({ error }: { error: st
43174327
<AlertDescription className="line-clamp-3" title={error}>
43184328
{error}
43194329
</AlertDescription>
4330+
{onDismiss && (
4331+
<AlertAction>
4332+
<button
4333+
type="button"
4334+
aria-label="Dismiss error"
4335+
className="inline-flex size-6 items-center justify-center rounded-md text-destructive/60 transition-colors hover:text-destructive"
4336+
onClick={onDismiss}
4337+
>
4338+
<XIcon className="size-3.5" />
4339+
</button>
4340+
</AlertAction>
4341+
)}
43204342
</Alert>
43214343
</div>
43224344
);
@@ -4732,7 +4754,7 @@ const EditableUserMessageBubble = memo(function EditableUserMessageBubble(props:
47324754
) : (
47334755
<>
47344756
{message.text && (
4735-
<pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
4757+
<pre className="whitespace-pre-wrap wrap-break-word font-mono text-base md:text-sm leading-relaxed text-foreground">
47364758
{message.text}
47374759
</pre>
47384760
)}
@@ -5078,7 +5100,7 @@ const ProposedPlanCard = memo(function ProposedPlanCard({
50785100
</div>
50795101
{canCollapse ? (
50805102
<div className="mt-4 flex justify-center">
5081-
<Button size="sm" variant="outline" onClick={() => setExpanded((value) => !value)}>
5103+
<Button size="sm" variant="outline" data-scroll-anchor-ignore onClick={() => setExpanded((value) => !value)}>
50825104
{expanded ? "Collapse plan" : "Expand plan"}
50835105
</Button>
50845106
</div>

0 commit comments

Comments
 (0)