-
Notifications
You must be signed in to change notification settings - Fork 12
t3code-inspired features: model slug, PR ahead hints, chat UX, settings #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
236f75a
aafb1f3
96c9035
0331e3a
10f18c2
c8f027a
84ca37f
f000eee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,12 @@ export function AppShell({ children }: { children: React.ReactNode }) { | |
| ); | ||
| const [dismissedContextBannerRoots, setDismissedContextBannerRoots] = | ||
| useState<Record<string, true>>({}); | ||
| /** Session dismiss for the “no AI provider” banner (per project root). */ | ||
| const [dismissedMissingAiBannerRoots, setDismissedMissingAiBannerRoots] = | ||
| useState<Record<string, true>>({}); | ||
| /** Session dismiss for the “GitHub not connected” banner (per project root). */ | ||
| const [dismissedGithubBannerRoots, setDismissedGithubBannerRoots] = | ||
| useState<Record<string, true>>({}); | ||
| const [projectMissing, setProjectMissing] = useState(false); | ||
| const [feedbackGenerating, setFeedbackGenerating] = useState(false); | ||
| const previousProjectRootRef = useRef<string | null | undefined>(undefined); | ||
|
|
@@ -480,6 +486,11 @@ export function AppShell({ children }: { children: React.ReactNode }) { | |
| setProjectMissing(false); | ||
| }, [project?.rootPath]); | ||
|
|
||
| useEffect(() => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] // apps/desktop/src/renderer/components/app/AppShell.tsx
useEffect(() => {
setDismissedMissingAiBannerRoots({});
setDismissedGithubBannerRoots({});
}, [project?.rootPath]);This effect clears all dismissed-banner entries every time the active |
||
| setDismissedMissingAiBannerRoots({}); | ||
| setDismissedGithubBannerRoots({}); | ||
| }, [project?.rootPath]); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| useEffect(() => { | ||
| const previousProjectRoot = previousProjectRootRef.current; | ||
| const nextProjectRoot = project?.rootPath ?? null; | ||
|
|
@@ -603,6 +614,12 @@ export function AppShell({ children }: { children: React.ReactNode }) { | |
| [missingContextDocs], | ||
| ); | ||
| const currentProjectRoot = project?.rootPath ?? null; | ||
| const missingAiBannerDismissed = Boolean( | ||
| currentProjectRoot && dismissedMissingAiBannerRoots[currentProjectRoot], | ||
| ); | ||
| const githubBannerDismissed = Boolean( | ||
| currentProjectRoot && dismissedGithubBannerRoots[currentProjectRoot], | ||
| ); | ||
| const contextBannerDismissed = Boolean( | ||
| currentProjectRoot && dismissedContextBannerRoots[currentProjectRoot], | ||
| ); | ||
|
|
@@ -791,12 +808,30 @@ export function AppShell({ children }: { children: React.ReactNode }) { | |
| !showWelcome && | ||
| aiStatusLoaded && | ||
| aiStatus !== null && | ||
| !hasAnyAiProvider ? ( | ||
| !hasAnyAiProvider && | ||
| !missingAiBannerDismissed ? ( | ||
| <div className="shrink-0 mx-2 mt-1 rounded bg-amber-500/6 px-3 py-1.5 text-[11px] font-mono text-amber-800"> | ||
| No AI provider is configured yet.{" "} | ||
| <Link to="/settings?tab=ai" className="underline"> | ||
| Set up AI | ||
| </Link> | ||
| <span> | ||
| No AI provider is configured yet.{" "} | ||
| <Link to="/settings?tab=ai" className="underline"> | ||
| Set up AI | ||
| </Link> | ||
| </span> | ||
| <button | ||
| type="button" | ||
| data-testid="dismiss-missing-ai-banner" | ||
| className="ml-2 text-amber-900/70 hover:text-amber-900" | ||
| onClick={() => { | ||
| if (!currentProjectRoot) return; | ||
| setDismissedMissingAiBannerRoots((prev) => ({ | ||
| ...prev, | ||
| [currentProjectRoot]: true, | ||
| })); | ||
| }} | ||
| title="Dismiss for this session" | ||
| > | ||
| × | ||
| </button> | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| </div> | ||
| ) : null} | ||
|
|
||
|
|
@@ -805,12 +840,30 @@ export function AppShell({ children }: { children: React.ReactNode }) { | |
| !showWelcome && | ||
| !isOnboardingRoute && | ||
| githubStatus !== null && | ||
| !githubStatus.tokenStored ? ( | ||
| !githubStatus.tokenStored && | ||
| !githubBannerDismissed ? ( | ||
| <div className="shrink-0 mx-3 mt-1.5 rounded bg-amber-500/6 px-3 py-1.5 text-[11px] font-mono text-amber-800"> | ||
| GitHub is not connected for this ADE app yet.{" "} | ||
| <Link to="/settings?tab=integrations" className="underline"> | ||
| Connect GitHub | ||
| </Link> | ||
| <span> | ||
| GitHub is not connected for this ADE app yet.{" "} | ||
| <Link to="/settings?tab=integrations" className="underline"> | ||
| Connect GitHub | ||
| </Link> | ||
| </span> | ||
| <button | ||
| type="button" | ||
| data-testid="dismiss-github-banner" | ||
| className="ml-2 text-amber-900/70 hover:text-amber-900" | ||
| onClick={() => { | ||
| if (!currentProjectRoot) return; | ||
| setDismissedGithubBannerRoots((prev) => ({ | ||
| ...prev, | ||
| [currentProjectRoot]: true, | ||
| })); | ||
| }} | ||
| title="Dismiss for this session" | ||
| > | ||
| × | ||
| </button> | ||
| </div> | ||
| ) : null} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,7 @@ import { ClaudeCacheTtlBadge } from "../shared/ClaudeCacheTtlBadge"; | |
| import { shouldShowClaudeCacheTtl } from "../../lib/claudeCacheTtl"; | ||
| import { getAgentChatModelsCached, getAiStatusCached } from "../../lib/aiDiscoveryCache"; | ||
| import { invalidateSessionListCache } from "../../lib/sessionListCache"; | ||
| import { playAgentTurnCompletionSound } from "../../lib/agentTurnCompletionSound"; | ||
|
|
||
| const LAST_MODEL_ID_KEY = "ade.chat.lastModelId"; | ||
| const LAST_REASONING_KEY_PREFIX = "ade.chat.lastReasoningEffort"; | ||
|
|
@@ -710,6 +711,7 @@ export function AgentChatPane({ | |
| onLaneChange?: (laneId: string) => void; | ||
| }) { | ||
| const projectRoot = useAppStore((s) => s.project?.rootPath ?? null); | ||
| const agentTurnCompletionSound = useAppStore((s) => s.agentTurnCompletionSound); | ||
| const navigate = useNavigate(); | ||
| const openAiProvidersSettings = useCallback(() => { | ||
| navigate("/settings?tab=ai#ai-providers"); | ||
|
|
@@ -777,6 +779,8 @@ export function AgentChatPane({ | |
| const shellRef = useRef<HTMLElement | null>(null); | ||
| const composerMaxHeightPx = layoutVariant === "grid-tile" ? 144 : null; | ||
| const sessionsRef = useRef<AgentChatSessionSummary[]>(sessions); | ||
| const completionSoundPrevTurnActiveRef = useRef(false); | ||
| const completionSoundArmedRef = useRef(true); | ||
|
|
||
| const appliedInitialSessionIdRef = useRef<string | null>(initialSessionId ?? null); | ||
| const loadedHistoryRef = useRef<Set<string>>(new Set()); | ||
|
|
@@ -824,6 +828,40 @@ export function AgentChatPane({ | |
| const pendingInput = selectedSessionId ? (pendingInputsBySession[selectedSessionId]?.[0] ?? null) : null; | ||
| const selectedSessionAwaitingInput = Boolean(pendingInput) || selectedSession?.awaitingInput === true; | ||
| const turnActive = selectedSessionId ? (turnActiveBySession[selectedSessionId] ?? false) : false; | ||
|
|
||
| useEffect(() => { | ||
| completionSoundPrevTurnActiveRef.current = false; | ||
| completionSoundArmedRef.current = true; | ||
| }, [selectedSessionId]); | ||
|
|
||
| useEffect(() => { | ||
| if (agentTurnCompletionSound === "off") { | ||
| completionSoundPrevTurnActiveRef.current = turnActive; | ||
| return; | ||
| } | ||
| if (turnActive) { | ||
| completionSoundArmedRef.current = true; | ||
| } | ||
| const sessionEnded = selectedSession?.status === "ended"; | ||
| const settled = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] The new completion check treats any transition from // apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
const settled =
Boolean(selectedSessionId)
&& !selectedSessionAwaitingInput
&& !sessionEnded;
const becameIdle = settled && prevTurn && !turnActive;This is too broad. I verified in @apps/desktop/src/renderer/components/chat/AgentChatPane.tsx:216-250 that |
||
| Boolean(selectedSessionId) | ||
| && !selectedSessionAwaitingInput | ||
| && !sessionEnded; | ||
| const prevTurn = completionSoundPrevTurnActiveRef.current; | ||
| const becameIdle = settled && prevTurn && !turnActive; | ||
| completionSoundPrevTurnActiveRef.current = turnActive; | ||
| if (becameIdle && completionSoundArmedRef.current) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] The new idle-transition effect treats any // apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
const prevTurn = completionSoundPrevTurnActiveRef.current;
const becameIdle = settled && prevTurn && !turnActive;
completionSoundPrevTurnActiveRef.current = turnActive;
if (becameIdle && completionSoundArmedRef.current) {
completionSoundArmedRef.current = false;
playAgentTurnCompletionSound(agentTurnCompletionSound);
}That also matches the existing |
||
| completionSoundArmedRef.current = false; | ||
| playAgentTurnCompletionSound(agentTurnCompletionSound); | ||
| } | ||
| }, [ | ||
| agentTurnCompletionSound, | ||
| selectedSessionId, | ||
| selectedSession?.status, | ||
| selectedSessionAwaitingInput, | ||
| turnActive, | ||
| ]); | ||
|
|
||
| const activeProviderConnection = selectedSession?.provider === "claude" | ||
| ? (providerConnections?.claude ?? null) | ||
| : selectedSession?.provider === "codex" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||
| import React, { Suspense, useCallback, useEffect, useState, useRef } from "react"; | ||||||||||||||||||||||||||
| import { CopySimple, Checks } from "@phosphor-icons/react"; | ||||||||||||||||||||||||||
| import { useAppStore, type CodeBlockCopyButtonPosition } from "../../state/appStore"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /* ── LRU cache for highlighted HTML ── */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -113,25 +114,47 @@ function DiffCodeBlock({ code }: { code: string }) { | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /* ── Copy button ── */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function CodeCopyButton({ code }: { code: string }) { | ||||||||||||||||||||||||||
| function copyTextToClipboard(text: string): Promise<boolean> { | ||||||||||||||||||||||||||
| if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { | ||||||||||||||||||||||||||
| return navigator.clipboard.writeText(text).then(() => true).catch(() => false); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const ta = document.createElement("textarea"); | ||||||||||||||||||||||||||
| ta.value = text; | ||||||||||||||||||||||||||
| ta.setAttribute("readonly", ""); | ||||||||||||||||||||||||||
| ta.style.position = "fixed"; | ||||||||||||||||||||||||||
| ta.style.left = "-9999px"; | ||||||||||||||||||||||||||
| document.body.appendChild(ta); | ||||||||||||||||||||||||||
| ta.select(); | ||||||||||||||||||||||||||
| const ok = document.execCommand("copy"); | ||||||||||||||||||||||||||
| document.body.removeChild(ta); | ||||||||||||||||||||||||||
| return Promise.resolve(ok); | ||||||||||||||||||||||||||
|
Comment on lines
+130
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] The new fallback branch appends a hidden // apps/desktop/src/renderer/components/chat/CodeHighlighter.tsx
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return Promise.resolve(ok);
Suggested change
|
||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| return Promise.resolve(false); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function CodeCopyButton({ code, position }: { code: string; position: CodeBlockCopyButtonPosition }) { | ||||||||||||||||||||||||||
| const [copied, setCopied] = useState(false); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const handleCopy = useCallback(() => { | ||||||||||||||||||||||||||
| if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) return; | ||||||||||||||||||||||||||
| void navigator.clipboard.writeText(code) | ||||||||||||||||||||||||||
| .then(() => { | ||||||||||||||||||||||||||
| void copyTextToClipboard(code) | ||||||||||||||||||||||||||
| .then((ok) => { | ||||||||||||||||||||||||||
| if (!ok) { | ||||||||||||||||||||||||||
| setCopied(false); | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| setCopied(true); | ||||||||||||||||||||||||||
| window.setTimeout(() => setCopied(false), 1_500); | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| .catch(() => { | ||||||||||||||||||||||||||
| setCopied(false); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }, [code]); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const posClass = position === "bottom" ? "bottom-2 top-auto" : "top-2"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||
| className="absolute right-2 top-2 z-10 inline-flex items-center gap-1 rounded-md border border-white/[0.08] bg-white/[0.03] px-1.5 py-0.5 font-sans text-[9px] text-fg/45 opacity-0 backdrop-blur-sm transition-all group-hover:opacity-100 hover:border-white/[0.14] hover:bg-white/[0.05] hover:text-fg/72" | ||||||||||||||||||||||||||
| className={`absolute right-2 z-10 inline-flex items-center gap-1 rounded-md border border-white/[0.08] bg-white/[0.03] px-1.5 py-0.5 font-sans text-[9px] text-fg/45 opacity-0 backdrop-blur-sm transition-all group-hover:opacity-100 [@media(hover:none)]:opacity-100 hover:border-white/[0.14] hover:bg-white/[0.05] hover:text-fg/72 ${posClass}`} | ||||||||||||||||||||||||||
| onClick={handleCopy} | ||||||||||||||||||||||||||
| title={copied ? "Copied" : "Copy code"} | ||||||||||||||||||||||||||
| aria-label={copied ? "Copied" : "Copy code"} | ||||||||||||||||||||||||||
|
|
@@ -223,12 +246,13 @@ export const HighlightedCode = React.memo(function HighlightedCode({ | |||||||||||||||||||||||||
| code: string; | ||||||||||||||||||||||||||
| language: string; | ||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||
| const copyButtonPosition = useAppStore((s) => s.codeBlockCopyButtonPosition); | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] This new store read only changes // apps/desktop/src/renderer/components/chat/CodeHighlighter.tsx
export const HighlightedCode = React.memo(function HighlightedCode({
code,
language,
}) {
const copyButtonPosition = useAppStore((s) => s.codeBlockCopyButtonPosition); |
||||||||||||||||||||||||||
| const trimmedCode = code.replace(/\n$/, ""); | ||||||||||||||||||||||||||
| const isDiff = language === "diff"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <div className="group relative my-3 overflow-hidden rounded-[10px] border border-[color:var(--chat-code-border)] bg-[var(--chat-code-bg)]"> | ||||||||||||||||||||||||||
| <CodeCopyButton code={trimmedCode} /> | ||||||||||||||||||||||||||
| <CodeCopyButton code={trimmedCode} position={copyButtonPosition} /> | ||||||||||||||||||||||||||
| <div className="overflow-x-auto whitespace-pre-wrap break-words px-4 py-3"> | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] In
{copyButtonPosition === "auto" && (
)}
```
|
||||||||||||||||||||||||||
| {isDiff ? ( | ||||||||||||||||||||||||||
| <DiffCodeBlock code={trimmedCode} /> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[🟡 Medium] [🔵 Bug]
This field is documented and displayed as "ahead of
defaultBaseBranch", but the new value is taken fromlane.status.ahead, which is not guaranteed to be measured against that branch. The changed code does:I verified
computeLaneStatus()in @apps/desktop/src/main/services/lanes/laneService.ts computesaheadfromgit rev-list ${baseRef}...${branchRef}, whileresolveStableLaneBaseBranch()in @apps/desktop/src/shared/laneBaseResolution.ts intentionally switches child lanes with non-primary parents to the parent branch. For stacked lanes wherelane.baseRefis still the stored/base branch, mobile will report commits ahead of the parent while actually counting commits ahead of the olderbaseRef, overstating the hint by including parent commits. Compute this count against the same branch returned byresolveStableLaneBaseBranch, or rename the field/message so it matches the underlying metric.