Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 38 additions & 21 deletions apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function App() {
const sidebarCollapsed = useCodesignStore((s) => s.sidebarCollapsed);
const activeReportLocalId = useCodesignStore((s) => s.activeReportLocalId);
const closeReportDialog = useCodesignStore((s) => s.closeReportDialog);
const isPromptExpanded = useCodesignStore((s) => s.isPromptExpanded);
const setPromptExpanded = useCodesignStore((s) => s.setPromptExpanded);

const [prompt, setPrompt] = useState('');
const [sidebarWidth, setSidebarWidth] = useState(() =>
Expand Down Expand Up @@ -84,27 +86,32 @@ export function App() {
if (view === 'workspace') setWorkspaceMounted(true);
}, [view]);

const onResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);

const onMove = (ev: MouseEvent) => {
const onResizeStart = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
const maxW = Math.round(window.innerWidth * 0.55);
const clamped = Math.min(Math.max(ev.clientX, 280), maxW);
setSidebarWidth(clamped);
};
const onUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}, []);
setSidebarWidth(Math.min(Math.max(e.clientX, 280), maxW));
setPromptExpanded(false);
setIsResizing(true);

const onMove = (ev: MouseEvent) => {
const clamped = Math.min(Math.max(ev.clientX, 280), Math.round(window.innerWidth * 0.55));
setSidebarWidth(clamped);
};
const onUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
},
[setPromptExpanded],
);

useEffect(() => {
async function bootstrap(): Promise<void> {
Expand Down Expand Up @@ -236,7 +243,17 @@ export function App() {
<div hidden={view !== 'workspace'} className="h-full flex flex-col">
<div className="flex-1 min-h-0 flex relative">
{isResizing && <div className="absolute inset-0 z-20 cursor-col-resize" />}
<div className="relative shrink-0" style={{ width: sidebarWidth }}>
<div
className={`relative shrink-0 ${isResizing ? '' : 'transition-[width] duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)]'}`}
style={{
width: isPromptExpanded
? Math.min(
Math.max(600, Math.round(window.innerWidth * 0.45)),
Math.round(window.innerWidth * 0.55),
)
: sidebarWidth,
}}
>
<Sidebar prompt={prompt} setPrompt={setPrompt} onSubmit={submit} />
<div
role="separator"
Expand Down
115 changes: 106 additions & 9 deletions apps/desktop/src/renderer/src/components/chat/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,42 @@ import { useCodesignStore } from '../../store';

const MAX_TEXTAREA_ROWS = 6;

function ExpandPromptIcon(): ReactNode {
return (
<svg
aria-hidden
viewBox="0 0 16 16"
className="h-[14px] w-[14px]"
fill="none"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="miter"
strokeWidth="1.6"
>
<path d="M4.25 6.5V4.25H6.5" />
<path d="M11.75 9.5v2.25H9.5" />
</svg>
);
}

function CollapsePromptIcon(): ReactNode {
return (
<svg
aria-hidden
viewBox="0 0 16 16"
className="h-[14px] w-[14px]"
fill="none"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="miter"
strokeWidth="1.6"
>
<path d="M6.5 3.75V6.5H3.75" />
<path d="M9.5 12.25V9.5h2.75" />
</svg>
);
}

export function getTextareaLineHeight(el: HTMLTextAreaElement): number {
const styles = getComputedStyle(el);
const lineHeight = Number.parseFloat(styles.lineHeight);
Expand All @@ -27,10 +63,28 @@ export function getTextareaLineHeight(el: HTMLTextAreaElement): number {
return fontSize * leading;
}

function resizeTextarea(el: HTMLTextAreaElement): void {
function resizeTextarea(el: HTMLTextAreaElement, isExpanded: boolean): boolean {
const rowHeight = getTextareaLineHeight(el);
el.style.height = 'auto';
el.style.height = `${Math.min(el.scrollHeight, rowHeight * MAX_TEXTAREA_ROWS)}px`;

// Measure exact text height silently without painting a jump or breaking transitions
const cachedTransition = el.style.transition;
const cachedHeight = el.style.height;

el.style.transition = 'none';
el.style.height = '1px';
const trueScrollHeight = el.scrollHeight;

el.style.height = cachedHeight;
// Force a layout recalc so the browser commits the origin height before transitioning
void el.offsetHeight;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void el.offsetHeight forces sync layout, and this function is called from onChange on each keystroke. This can cause typing jank for long prompts; prefer a non-forced measurement/update path.

el.style.transition = cachedTransition;

if (isExpanded) {
el.style.height = `${Math.min(Math.max(trueScrollHeight, window.innerHeight * 0.4), window.innerHeight * 0.6)}px`;
} else {
el.style.height = `${Math.min(trueScrollHeight, rowHeight * MAX_TEXTAREA_ROWS)}px`;
}
return trueScrollHeight > rowHeight * 2.5;
}

export interface PromptInputProps {
Expand Down Expand Up @@ -106,9 +160,31 @@ export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(funct
? `${elapsedSec}s`
: `${Math.floor(elapsedSec / 60)}:${String(elapsedSec % 60).padStart(2, '0')}`;

const isExpanded = useCodesignStore((s) => s.isPromptExpanded);
const setIsExpanded = useCodesignStore((s) => s.setPromptExpanded);
const [showExpandIcon, setShowExpandIcon] = useState(false);

useEffect(() => {
const textarea = taRef.current;
if (textarea && textarea.value === prompt) {
const isOver = resizeTextarea(textarea, isExpanded);
setShowExpandIcon(isOver);
if (isExpanded) {
textarea.focus();
}
}
}, [prompt, isExpanded]);

useEffect(() => {
if (taRef.current) resizeTextarea(taRef.current);
}, []);
if (!isExpanded) return;
const handleDocKeyDown = (e: globalThis.KeyboardEvent) => {
if (e.key === 'Escape') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape here collapses the prompt but still bubbles to window-level escape bindings in App, which can also close dialogs/switch modes. Please consume the event (preventDefault + stopPropagation) when expanded composer handles Escape.

setIsExpanded(false);
}
};
document.addEventListener('keydown', handleDocKeyDown);
return () => document.removeEventListener('keydown', handleDocKeyDown);
}, [isExpanded, setIsExpanded]);

useImperativeHandle(ref, () => ({
focus: () => {
Expand Down Expand Up @@ -139,7 +215,9 @@ export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(funct

return (
<form onSubmit={handleSubmit}>
<div className="relative rounded-[16px] bg-[var(--color-surface)] border-[1.5px] border-[var(--color-border-muted)] focus-within:border-[var(--color-accent)] transition-colors duration-150 ease-out">
<div
className={`relative rounded-[16px] bg-[var(--color-surface)] border-[1.5px] border-[var(--color-border-muted)] focus-within:border-[var(--color-accent)] transition-[border-color,shadow] duration-150 ease-out ${isExpanded ? 'shadow-[var(--shadow-elevated)]' : ''}`}
>
{contextSummary ? (
<div className="border-b border-[var(--color-border-subtle)] px-[12px] py-[10px]">
{contextSummary}
Expand All @@ -150,15 +228,34 @@ export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(funct
value={prompt}
onChange={(e) => {
setPrompt(e.target.value);
resizeTextarea(e.currentTarget);
const isOver = resizeTextarea(e.currentTarget, isExpanded);
setShowExpandIcon(isOver);
}}
onKeyDown={handleKeyDown}
placeholder={t('chat.placeholderRich')}
rows={1}
className="codesign-prompt-textarea block w-full resize-none appearance-none border-0 bg-transparent px-[14px] pt-[12px] pb-[44px] text-[14px] leading-[1.55] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] shadow-none outline-none focus:outline-none focus:ring-0 min-h-[24px] overflow-y-auto"
style={{ fontFamily: 'var(--font-sans)' }}
className="codesign-prompt-textarea block w-full resize-none appearance-none border-0 bg-transparent pl-[14px] pr-[36px] pt-[12px] pb-[44px] text-[14px] leading-[1.55] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] shadow-none outline-none focus:outline-none focus:ring-0 min-h-[24px] overflow-y-auto"
style={{
fontFamily: 'var(--font-sans)',
transition: 'height 0.2s cubic-bezier(0.2,0.8,0.2,1)',
}}
/>

<div
className={`absolute top-[8px] right-[8px] transition-opacity duration-200 z-[2] ${showExpandIcon || isExpanded ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
>
<Tooltip label={isExpanded ? t('chat.collapse') : t('chat.expand')} side="top">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="p-1.5 rounded-md text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors"
aria-label={isExpanded ? t('chat.collapse') : t('chat.expand')}
>
{isExpanded ? <CollapsePromptIcon /> : <ExpandPromptIcon />}
</button>
</Tooltip>
</div>

{leadingAction ? (
<div className="absolute bottom-[8px] left-[8px]">{leadingAction}</div>
) : null}
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/renderer/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ interface CodesignState {
* only persisted to SQLite when the result arrives (done/error). */
pendingToolCalls: ChatToolCallPayload[];
sidebarCollapsed: boolean;
isPromptExpanded: boolean;

// Workstream D — comments
comments: CommentRow[];
Expand Down Expand Up @@ -404,6 +405,7 @@ interface CodesignState {
* panel to write a re-serialized EDITMODE block back into the artifact. */
setPreviewHtml: (content: string) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
setPromptExpanded: (val: boolean) => void;

// Workstream D — comments
loadCommentsForCurrentDesign: () => Promise<void>;
Expand Down Expand Up @@ -1217,6 +1219,7 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
chatMessages: [],
chatLoaded: false,
sidebarCollapsed: false,
isPromptExpanded: false,

comments: [],
commentsLoaded: false,
Expand Down Expand Up @@ -2324,6 +2327,10 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
set({ sidebarCollapsed: collapsed });
},

setPromptExpanded(val: boolean) {
set({ isPromptExpanded: val });
},

async loadCommentsForCurrentDesign() {
if (!window.codesign) return;
const designId = get().currentDesignId;
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"sendAnywhere": "anywhere",
"send": "Send prompt",
"stop": "Stop generation",
"expand": "Expand prompt",
"collapse": "Collapse prompt",
"placeholderRich": "Describe a design… try 'Pitch deck for a fintech startup'"
},
"sidebar": {
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"sendAnywhere": "em qualquer lugar",
"send": "Enviar prompt",
"stop": "Parar geração",
"expand": "Expandir prompt",
"collapse": "Recolher prompt",
"placeholderRich": "Descreva um design… tente 'Pitch deck para uma startup fintech'"
},
"sidebar": {
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"sendAnywhere": "任意位置发送",
"send": "发送提示词",
"stop": "停止生成",
"expand": "展开提示词",
"collapse": "收起提示词",
"placeholderRich": "描述你想要的设计… 试试「AI 创业公司的落地页」"
},
"sidebar": {
Expand Down
Loading