opentui(v2): home hint (item 12) + verify /goal + expand feature matrix (item 8)
Item 12 — the missing helper/home screen: view/homeHint.tsx renders on an empty
transcript (Ink helpHint.tsx parity) — brand line, common commands (/help /model
/sessions /skills /agents /clear), and input tips (type · ↑↓ history · @file ·
Ctrl+C). Decorative → selectable={false}. Replaced by the transcript on the first
turn.
Item 8 — /goal verified live: slash.exec rejects it (pending-input) → dispatch
falls to command.dispatch {name:'goal'} → {type:'send', notice:'⊙ Goal set…',
message} → notice shown + the goal turn submitted (handleDispatchResult). Wired.
Docs: opentui-feature-map.md gains the full 15-item live-feedback parity matrix
(Ink/opencode primitive · v2 file · status); opentui-smoke.md gains the 15-item
run log. All 15 items ✅ (image-paste wired but unverified in the clipboard-less
CI env).
Tests: home-hint render. 72 pass. Live-smoked: empty launch shows the home hint.
This commit is contained in:
parent
c3d2d87a74
commit
6e3915fbc1
@ -177,6 +177,28 @@ describe('App render (Phase 1, themed)', () => {
|
|||||||
expect(frame).toContain('Tab complete') // dropdown hint
|
expect(frame).toContain('Tab complete') // dropdown hint
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the empty transcript shows the home hint (item 12)', async () => {
|
||||||
|
const store = createSessionStore()
|
||||||
|
store.apply({ type: 'gateway.ready' })
|
||||||
|
|
||||||
|
const frame = await captureFrame(
|
||||||
|
() => (
|
||||||
|
<ThemeProvider theme={() => store.state.theme}>
|
||||||
|
<App store={store} />
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
{ until: '/help', width: 72, height: 20 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// (theme-independent assertions — testRender reuses a global root, so a prior
|
||||||
|
// test's skin/brand can bleed; the real app has one store. The home hint's
|
||||||
|
// content is what matters here.)
|
||||||
|
expect(frame).toContain('/help') // common command
|
||||||
|
expect(frame).toContain('/agents')
|
||||||
|
expect(frame).toContain('resume a session')
|
||||||
|
expect(frame).toContain('to mention') // the input tips line
|
||||||
|
})
|
||||||
|
|
||||||
test('the status bar renders model · context% · cwd (item 14)', async () => {
|
test('the status bar renders model · context% · cwd (item 14)', async () => {
|
||||||
const store = createSessionStore()
|
const store = createSessionStore()
|
||||||
store.apply({ type: 'gateway.ready' })
|
store.apply({ type: 'gateway.ready' })
|
||||||
|
|||||||
50
ui-tui-opentui-v2/src/view/homeHint.tsx
Normal file
50
ui-tui-opentui-v2/src/view/homeHint.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* HomeHint — the empty-transcript home screen (item 12; Ink's `helpHint.tsx`).
|
||||||
|
* Shown when there are no messages yet: the brand line, a few common commands,
|
||||||
|
* and the key input tips. Replaced by the transcript as soon as a turn lands.
|
||||||
|
* Fully themed; decorative, so `selectable={false}` (item 4).
|
||||||
|
*/
|
||||||
|
import { For } from 'solid-js'
|
||||||
|
|
||||||
|
import { useTheme } from './theme.tsx'
|
||||||
|
|
||||||
|
const COMMANDS: ReadonlyArray<readonly [string, string]> = [
|
||||||
|
['/help', 'list all commands'],
|
||||||
|
['/model', 'switch model'],
|
||||||
|
['/sessions', 'resume a session'],
|
||||||
|
['/skills', 'browse skills'],
|
||||||
|
['/agents', 'live delegation trace'],
|
||||||
|
['/clear', 'clear the transcript']
|
||||||
|
]
|
||||||
|
|
||||||
|
export function HomeHint() {
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<box style={{ flexDirection: 'column', flexShrink: 0, paddingLeft: 1, marginTop: 1 }}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.accent }}>{theme().brand.icon} </span>
|
||||||
|
<b>{theme().brand.name}</b>
|
||||||
|
</text>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{theme().brand.welcome}</span>
|
||||||
|
</text>
|
||||||
|
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
||||||
|
<For each={COMMANDS}>
|
||||||
|
{([cmd, desc]) => (
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.label }}>{cmd.padEnd(11)}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{desc}</span>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
<box style={{ marginTop: 1 }}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>
|
||||||
|
Type to chat · ↑↓ history · @file to mention · Ctrl+C to stop/quit
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -11,15 +11,20 @@
|
|||||||
* measurement → phantom scroll offset that clips the top + leaves a gap),
|
* measurement → phantom scroll offset that clips the top + leaves a gap),
|
||||||
* - `stickyScroll` + `stickyStart="bottom"` to pin the latest line.
|
* - `stickyScroll` + `stickyStart="bottom"` to pin the latest line.
|
||||||
*/
|
*/
|
||||||
import { For } from 'solid-js'
|
import { For, Show } from 'solid-js'
|
||||||
|
|
||||||
import type { SessionStore } from '../logic/store.ts'
|
import type { SessionStore } from '../logic/store.ts'
|
||||||
|
import { HomeHint } from './homeHint.tsx'
|
||||||
import { MessageLine } from './messageLine.tsx'
|
import { MessageLine } from './messageLine.tsx'
|
||||||
|
|
||||||
export function Transcript(props: { store: SessionStore }) {
|
export function Transcript(props: { store: SessionStore }) {
|
||||||
return (
|
return (
|
||||||
<box style={{ flexGrow: 1, minHeight: 0, marginTop: 1 }}>
|
<box style={{ flexGrow: 1, minHeight: 0, marginTop: 1 }}>
|
||||||
<scrollbox style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
|
<scrollbox style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
|
||||||
|
{/* empty-transcript home screen (item 12); replaced by messages on the first turn */}
|
||||||
|
<Show when={props.store.state.messages.length === 0}>
|
||||||
|
<HomeHint />
|
||||||
|
</Show>
|
||||||
<For each={props.store.state.messages}>{message => <MessageLine message={message} />}</For>
|
<For each={props.store.state.messages}>{message => <MessageLine message={message} />}</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user