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
11 changes: 11 additions & 0 deletions apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { toast } from "sonner"

import { ScrollArea } from "@/components/ui/scroll-area"
import { cn } from "@/lib/utils"
import { EmptyStateSuggestions } from "@/components/app-shell/EmptyStateSuggestions"
import { Markdown, CollapsibleMarkdownProvider, StreamingMarkdown, type RenderMode } from "@/components/markdown"
import { AnimatedCollapsibleContent } from "@/components/ui/collapsible"
import {
Expand Down Expand Up @@ -1683,6 +1684,16 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
{t('chat.emptyTitle')}
</h1>
{renderChatInputZone('mt-0 px-0 pb-0 @xs/panel:px-0')}
{(inputValue ?? '').trim().length === 0 ? (
<EmptyStateSuggestions
disabled={isInputDisabled || disableSend || connectionUnavailable}
onSelect={(prompt) => {
onInputChange?.(prompt)
// Let the controlled value propagate to the composer, then focus.
requestAnimationFrame(() => textareaRef.current?.focus())
}}
/>
) : null}
</motion.div>
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { BookOpen, Hammer, Bug, FlaskConical, type LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'

/**
* Starter prompt suggestions shown on the empty conversation state, beneath the
* centered composer. Mirrors the "prompt suggestions" surface in Claude Code
* Desktop / ChatGPT / Codex: a few clickable starting points that populate the
* composer (they do NOT auto-send, so the user can edit before submitting).
*
* Each suggestion pairs a short, localized label with a fuller prompt. Clicking
* one calls `onSelect(prompt)` — the parent seeds the (controlled) composer via
* the same draft channel used elsewhere, then focuses the input.
*/

interface SuggestionDef {
/** Stable id → i18n keys `chat.suggestions.<id>.title` / `.prompt`. */
id: string
icon: LucideIcon
}

/** The fixed set of starter suggestions. Order here is the render order. */
const SUGGESTIONS: readonly SuggestionDef[] = [
{ id: 'explain', icon: BookOpen },
{ id: 'build', icon: Hammer },
{ id: 'fix', icon: Bug },
{ id: 'tests', icon: FlaskConical },
] as const

export interface EmptyStateSuggestionsProps {
/** Called with the full prompt text when a suggestion is chosen. */
onSelect: (prompt: string) => void
/** Hide the surface entirely (e.g. no connection / input disabled). */
disabled?: boolean
className?: string
}

export function EmptyStateSuggestions({
onSelect,
disabled = false,
className,
}: EmptyStateSuggestionsProps) {
const { t } = useTranslation()

if (disabled) return null

return (
<div
data-testid="empty-suggestions"
className={cn(
'mx-auto mt-4 grid w-full max-w-[840px] grid-cols-1 gap-2 sm:grid-cols-2',
className,
)}
>
{SUGGESTIONS.map(({ id, icon: Icon }) => {
const title = t(`chat.suggestions.${id}.title`)
const prompt = t(`chat.suggestions.${id}.prompt`)
return (
<button
key={id}
type="button"
data-testid="empty-suggestion"
data-suggestion-id={id}
onClick={() => onSelect(prompt)}
className={cn(
'group flex items-center gap-2.5 rounded-[10px] border bg-muted/20 px-3.5 py-2.5 text-left',
'text-sm text-foreground/80 transition-colors',
'hover:bg-muted/50 hover:text-foreground',
'focus:outline-none focus-visible:ring-1 focus-visible:ring-ring',
)}
>
<Icon className="h-4 w-4 shrink-0 text-muted-foreground group-hover:text-foreground" />
<span className="min-w-0 truncate font-medium">{title}</span>
</button>
)
})}
</div>
)
}
14 changes: 13 additions & 1 deletion docs/loop/feature-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,19 @@ log, not the system of record.

| slug | title | source | feasibility | status | issue | pr | branch | updated | notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). |
| starter-suggestions | Starter prompt suggestions on the empty conversation | Claude Code Desktop "prompt suggestions" / ChatGPT & Codex example prompts | frontend-only | pr-open | [#72](https://github.com/modelstudioai/openwork/issues/72) | [#73](https://github.com/modelstudioai/openwork/pull/73) | loop/starter-suggestions | 2026-07-09 | New `EmptyStateSuggestions` chip row rendered under the centered empty-state composer in `ChatDisplay` (only while composer is empty). Clicking a chip seeds the composer via the existing controlled draft channel (`onInputChange`) + focuses it — does NOT auto-send. 4 suggestions, 8 new i18n keys ×7 locales. typecheck:all / `bun test` zero-delta vs main (11 electron-typecheck + 56-test pre-existing baseline byte-identical); renderer build ✅; i18n parity ✅; lint 0 errors on touched files. CDP assertion `e2e/assertions/starter-suggestions.assert.ts` included; **could not execute locally** (Electron binary egress-blocked: `github.com` releases 403). |
| copy-as-markdown | "Copy" (copy-as-Markdown) action on assistant/agent responses | Claude/ChatGPT/Codex desktop message copy | frontend-only | pr-open | [#70](https://github.com/modelstudioai/openwork/issues/70) | [#71](https://github.com/modelstudioai/openwork/pull/71) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| interface-zoom | Interface zoom (⌘+ / ⌘- / ⌘0) to scale the whole app | Claude/VS Code/Codex desktop zoom | frontend-only | pr-open | [#68](https://github.com/modelstudioai/openwork/issues/68) | [#69](https://github.com/modelstudioai/openwork/pull/69) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| increase-contrast | "Increase contrast" accessibility setting in Appearance | macOS / Windows / Claude desktop increase-contrast | frontend-only | pr-open | [#66](https://github.com/modelstudioai/openwork/issues/66) | [#67](https://github.com/modelstudioai/openwork/pull/67) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| chat-text-size | "Chat text size" (Small / Default / Large) in Appearance | ChatGPT / Claude desktop text-size | frontend-only | pr-open | [#64](https://github.com/modelstudioai/openwork/issues/64) | [#65](https://github.com/modelstudioai/openwork/pull/65) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| conversation-width | "Conversation width" (Comfortable / Wide / Full) in Appearance | ChatGPT / Claude desktop conversation width | frontend-only | pr-open | [#62](https://github.com/modelstudioai/openwork/issues/62) | [#63](https://github.com/modelstudioai/openwork/pull/63) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| composer-word-count | Live word / character count in the chat composer | ChatGPT / editors composer counters | frontend-only | pr-open | [#60](https://github.com/modelstudioai/openwork/issues/60) | [#61](https://github.com/modelstudioai/openwork/pull/61) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | VS Code / Codex desktop shortcuts search | frontend-only | pr-open | [#58](https://github.com/modelstudioai/openwork/issues/58) | [#59](https://github.com/modelstudioai/openwork/pull/59) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| recent-commands | Surface recently-used commands in the Command Palette (⌘K) | Claude Code Desktop / VS Code recent commands | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| thinking-menu-shortcut | Keyboard shortcut (⌘⇧E) to open the composer thinking menu | Claude Code Desktop effort menu ⌘⇧E | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-menu-shortcut | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| prompt-history | Recall previously-sent prompts with Up / Down in the composer | Claude Code / ChatGPT / shell history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) | — | 2026-07-09 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| session-search-shortcut | Cmd+F / `app.search` should activate in-conversation session search | OpenWork internal bug report | frontend-only | proposed | [#43](https://github.com/modelstudioai/openwork/issues/43) | — | — | 2026-07-09 | Open loop-bot issue (bug), no PR yet. Not selected this round. |
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | merged | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-09 | **Merged** into `main`. Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. |
| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. |
| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. |
| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |
Expand Down
107 changes: 107 additions & 0 deletions e2e/assertions/starter-suggestions.assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Feature assertion: starter prompt suggestions on the empty conversation state.
*
* Drives the real built app over CDP through the full path:
* boot into the empty draft chat → the suggestion chips render → click one →
* the composer is populated with that suggestion's (fuller) prompt → the
* suggestions surface disappears once the composer has content.
*
* This proves the feature actually *does* something (seeds the composer), not
* merely that the chips render.
*/

import type { Assertion } from '../runner';

const SUGGESTIONS = '[data-testid="empty-suggestions"]';
const CHIP = '[data-testid="empty-suggestion"]';
const COMPOSER = '[role="textbox"][aria-multiline="true"]';

/** Visible chips only (defensive against hidden/duplicated nodes). */
const VISIBLE_CHIPS_EXPR = `[...document.querySelectorAll(${JSON.stringify(
CHIP,
)})].filter((el) => el.offsetParent !== null)`;

/** Trimmed text of the (visible) composer, with zero-width chars stripped. */
const COMPOSER_TEXT_EXPR = `(() => {
const el = [...document.querySelectorAll(${JSON.stringify(
COMPOSER,
)})].find((n) => n.offsetParent !== null);
if (!el) return null;
return (el.textContent || '').replace(/[\\u200B-\\u200D\\uFEFF]/g, '').trim();
})()`;

const assertion: Assertion = {
name: 'empty-state starter suggestions seed the composer',
async run(app) {
const { session } = app;

// App fully mounted.
await session.waitForFunction(
'!document.getElementById("_loader") && (document.getElementById("root")?.childElementCount ?? 0) > 0',
{ timeoutMs: 30000, message: 'React UI did not mount' },
);

// Reach the ready AppShell (not onboarding / workspace-picker). The empty
// draft chat — with its centered composer — is the default landing view.
await session.waitForSelector('[aria-label="Craft menu"]', {
timeoutMs: 30000,
message: 'app did not reach the ready AppShell state',
});

// 1. The suggestions surface renders on the empty conversation.
await session.waitForSelector(SUGGESTIONS, {
timeoutMs: 15000,
message: 'starter suggestions did not render on the empty conversation',
});

// 2. It shows the full set of chips (4).
await session.waitForFunction(`${VISIBLE_CHIPS_EXPR}.length === 4`, {
timeoutMs: 8000,
message: 'expected 4 visible starter-suggestion chips',
});

// 3. The composer starts empty.
const before = await session.evaluate<string | null>(COMPOSER_TEXT_EXPR);
if (before == null) throw new Error('could not locate the composer text box');
if (before.length !== 0) {
throw new Error(`composer was not empty at start (saw: ${JSON.stringify(before)})`);
}

// 4. Capture the first chip's visible label, then click it.
const label = await session.evaluate<string | null>(
`(() => { const el = ${VISIBLE_CHIPS_EXPR}[0]; return el ? (el.textContent || '').trim() : null; })()`,
);
if (!label) throw new Error('could not read the first suggestion label');

const clicked = await session.evaluate<boolean>(
`(() => { const el = ${VISIBLE_CHIPS_EXPR}[0]; if (!el) return false; el.click(); return true; })()`,
);
if (!clicked) throw new Error('failed to click the first suggestion chip');

// 5. The composer is populated with the suggestion's prompt. The prompt is a
// full sentence, so it must be non-empty and longer than the short label —
// proving it seeded the *prompt*, not merely echoed the chip title.
await session.waitForFunction(
`(() => {
const text = ${COMPOSER_TEXT_EXPR};
return typeof text === 'string' && text.length > ${label.length} && text.length > 20;
})()`,
{
timeoutMs: 8000,
message: 'clicking a suggestion did not populate the composer with its prompt',
},
);

// 6. Once the composer has content, the suggestions surface goes away
// (matching the empty-state-only behavior of the feature).
await session.waitForFunction(
`!document.querySelector(${JSON.stringify(SUGGESTIONS)})`,
{
timeoutMs: 8000,
message: 'suggestions did not hide after the composer was populated',
},
);
},
};

export default assertion;
8 changes: 8 additions & 0 deletions packages/shared/src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@
"chat.contextUsage.title": "Kontextfenster",
"chat.contextUsage.usedOfTotal": "{{used}} verwendet, {{total}} gesamt",
"chat.emptyTitle": "Was sollen wir bauen?",
"chat.suggestions.explain.title": "Diese Codebasis erklären",
"chat.suggestions.explain.prompt": "Gib mir einen Überblick über diese Codebasis – ihren Zweck, die wichtigsten Komponenten und wie die Teile zusammenspielen.",
"chat.suggestions.build.title": "Ein Feature bauen",
"chat.suggestions.build.prompt": "Hilf mir, ein neues Feature zu bauen. Frag mich, was es können soll, schlage dann einen Ansatz vor und setze ihn um.",
"chat.suggestions.fix.title": "Einen Bug beheben",
"chat.suggestions.fix.prompt": "Hilf mir, einen Bug zu finden und zu beheben. Ich beschreibe das Problem und du untersuchst den relevanten Code.",
"chat.suggestions.tests.title": "Tests schreiben",
"chat.suggestions.tests.prompt": "Schreibe Tests für einen Teil meines Projekts. Sag mir, was abzudecken ist, und füge gründliche, bestehende Tests hinzu.",
"chat.enterSessionName": "Sitzungsname eingeben...",
"chat.failedToStopSharing": "Freigabe konnte nicht beendet werden",
"chat.failedToUpdateShare": "Freigabe konnte nicht aktualisiert werden",
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@
"chat.contextUsage.title": "Context window",
"chat.contextUsage.usedOfTotal": "{{used}} used, {{total}} total",
"chat.emptyTitle": "What should we build?",
"chat.suggestions.explain.title": "Explain this codebase",
"chat.suggestions.explain.prompt": "Give me a high-level overview of this codebase — its purpose, main components, and how the pieces fit together.",
"chat.suggestions.build.title": "Build a feature",
"chat.suggestions.build.prompt": "Help me build a new feature. Ask me what it should do, then propose an approach and implement it.",
"chat.suggestions.fix.title": "Fix a bug",
"chat.suggestions.fix.prompt": "Help me track down and fix a bug. I'll describe what's going wrong and you investigate the relevant code.",
"chat.suggestions.tests.title": "Write tests",
"chat.suggestions.tests.prompt": "Write tests for a part of my project. Point me at what to cover and add thorough, passing tests.",
"chat.enterSessionName": "Enter session name...",
"chat.failedToStopSharing": "Failed to stop sharing",
"chat.failedToUpdateShare": "Failed to update share",
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@
"chat.contextUsage.title": "Ventana de contexto",
"chat.contextUsage.usedOfTotal": "{{used}} usados, {{total}} en total",
"chat.emptyTitle": "¿Qué deberíamos construir?",
"chat.suggestions.explain.title": "Explicar este código",
"chat.suggestions.explain.prompt": "Dame una visión general de este código: su propósito, sus componentes principales y cómo encajan las piezas.",
"chat.suggestions.build.title": "Crear una función",
"chat.suggestions.build.prompt": "Ayúdame a crear una nueva función. Pregúntame qué debe hacer, propón un enfoque e impleméntalo.",
"chat.suggestions.fix.title": "Corregir un error",
"chat.suggestions.fix.prompt": "Ayúdame a localizar y corregir un error. Yo describo qué falla y tú investigas el código relevante.",
"chat.suggestions.tests.title": "Escribir pruebas",
"chat.suggestions.tests.prompt": "Escribe pruebas para una parte de mi proyecto. Indícame qué cubrir y añade pruebas completas que pasen.",
"chat.enterSessionName": "Introduce el nombre de la sesión...",
"chat.failedToStopSharing": "Error al detener la compartición",
"chat.failedToUpdateShare": "Error al actualizar el compartido",
Expand Down
Loading