mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
The desktop app now keeps the composer stable while turns are active, reduces low-signal tool noise in the transcript, restores project context under the composer after session creation, and relies on the CLI's own permission requests instead of injecting broader desktop-side Bash asks. This also brings in the supporting desktop app source tree and the server routes/session metadata needed for git info, filesystem browsing, session resume, slash commands, and SDK-backed permission bridging so the UI can operate as a coherent feature instead of a partial patch. Constraint: Desktop transcript needs to stay usable during long multi-tool sessions without hiding file-change diffs Constraint: Permission prompts must mirror CLI behavior closely enough that read-only commands do not get desktop-only prompts Rejected: Keep rendering Read/Bash bodies inline | too noisy and unlike the intended transcript model Rejected: Commit only the touched desktop files | would leave the newly introduced desktop app incomplete in git history Confidence: medium Scope-risk: broad Reversibility: messy Directive: Treat non-writing tools as summary-first transcript events; do not re-expand them by default without validating the UX against long sessions Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- --run Tested: bun test src/server/__tests__/conversations.test.ts Not-tested: Manual visual regression against the exact screenshots in a live desktop session Not-tested: Full root TypeScript check (repository still has unrelated extracted-native parse failures)
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
|
|
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
|
|
|
|
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
variant?: ButtonVariant
|
|
size?: 'sm' | 'md' | 'lg'
|
|
loading?: boolean
|
|
icon?: ReactNode
|
|
}
|
|
|
|
const variantStyles: Record<ButtonVariant, string> = {
|
|
primary:
|
|
'bg-[var(--color-btn-primary-bg)] text-[var(--color-btn-primary-fg)] hover:bg-[var(--color-btn-primary-bg-hover)] active:bg-[var(--color-btn-primary-bg-active)]',
|
|
secondary:
|
|
'bg-[var(--color-surface)] text-[var(--color-text-primary)] border border-[var(--color-border)] hover:bg-[var(--color-surface-hover)]',
|
|
danger:
|
|
'bg-[var(--color-error)] text-white hover:opacity-90',
|
|
ghost:
|
|
'bg-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]',
|
|
}
|
|
|
|
const sizeStyles = {
|
|
sm: 'px-2 py-1 text-xs',
|
|
md: 'px-4 py-2 text-sm',
|
|
lg: 'px-5 py-2.5 text-sm',
|
|
}
|
|
|
|
export function Button({
|
|
variant = 'primary',
|
|
size = 'md',
|
|
loading = false,
|
|
icon,
|
|
disabled,
|
|
children,
|
|
className = '',
|
|
...props
|
|
}: ButtonProps) {
|
|
return (
|
|
<button
|
|
disabled={disabled || loading}
|
|
className={`
|
|
inline-flex items-center justify-center gap-1.5 rounded-[var(--radius-md)]
|
|
font-medium transition-colors duration-150 cursor-pointer
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
${variantStyles[variant]} ${sizeStyles[size]} ${className}
|
|
`}
|
|
{...props}
|
|
>
|
|
{loading ? <Spinner /> : icon}
|
|
{children}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
function Spinner() {
|
|
return (
|
|
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
)
|
|
}
|