feat(tui-go): idle launcher — centered input + status/keys rail (Phase 1)
Replace the idle two-panel layout (session list + welcome, where the welcome panel duplicated the list) with an opencode-style launcher: a compact, centered input whose lower-right shows the launch target and model (chat by default), a hideable right rail of status + quick keys, and no session list — it's a keypress away via R. - Tab (or w) cycles the launch target: chat → workflows → chat (launcherWf), shown at the input. Enter on chat starts a chat; on a workflow, StartSession with the typed text as the brief. - Right rail (connection, session/active counts, i/Tab/R/?/p keys) hides via the new "rail" palette command. Drops automatically on narrow terminals. - View() suppresses the bottom input bar on idle (the input is the launcher); footer + ? help updated to the launcher keys (help-coverage test extended). The old idle-panel renderers (sessionRows/welcomeRows/workflowRows) are now unused but kept in the tree pending Phase 2 + a decision on where the session dates should live now that the idle list is gone. Multi-line input (Ctrl+J / Alt+Enter newline) is Phase 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,12 @@ func (m Model) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
// On the idle launcher, Tab cycles the launch target (chat → workflows → chat) in any
|
||||
// edit mode, so it works whether or not the input is focused.
|
||||
if m.displayState() == StateIdle && k.Type == tea.KeyTab {
|
||||
m.cycleLauncherWf()
|
||||
return m, nil
|
||||
}
|
||||
if m.editMode == ModeInsert {
|
||||
return m.handleInsertKey(k)
|
||||
}
|
||||
@@ -300,13 +306,9 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "m":
|
||||
m.openModelsOverlay()
|
||||
case "w":
|
||||
// Launcher: w is an alias for Tab — cycle the launch target (chat / workflow…).
|
||||
if ds == StateIdle {
|
||||
m.wfVisible = !m.wfVisible
|
||||
if m.wfVisible {
|
||||
m.wfIndex = 0
|
||||
} else {
|
||||
m.wfIndex = -1
|
||||
}
|
||||
m.cycleLauncherWf()
|
||||
}
|
||||
case "l":
|
||||
if ds != StateIdle {
|
||||
@@ -1074,6 +1076,7 @@ func paletteCommands() []paletteCmd {
|
||||
{"config", "g", "edit config", "view / change correx settings"},
|
||||
{"grants", "G", "grants", "view / revoke standing grants"},
|
||||
{"statusbar", "", "status bar", "show / hide status-bar segments"},
|
||||
{"rail", "", "idle rail", "show / hide the idle status + keys rail"},
|
||||
{"actions", "", "inline actions", "show / hide tool & action rows in output"},
|
||||
{"help", "?", "help", "keybinding cheat-sheet"},
|
||||
{"mode", "s", "toggle mode", "switch chat / steering"},
|
||||
@@ -1122,6 +1125,8 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
|
||||
case "statusbar":
|
||||
m.overlay = OverlayStatusbar
|
||||
m.sbIndex = 0
|
||||
case "rail":
|
||||
m.railHidden = !m.railHidden
|
||||
case "actions":
|
||||
m.toggleInlineActions()
|
||||
case "help":
|
||||
@@ -1164,6 +1169,11 @@ func (m *Model) backspace() {
|
||||
m.inputCursor = c - 1
|
||||
}
|
||||
|
||||
// cycleLauncherWf advances the idle launch target: 0 = chat, 1..N = workflows[i-1], wrapping.
|
||||
func (m *Model) cycleLauncherWf() {
|
||||
m.launcherWf = (m.launcherWf + 1) % (len(m.workflows) + 1)
|
||||
}
|
||||
|
||||
func (m *Model) cycleChatMode() {
|
||||
if m.chatMode == ChatModeChat {
|
||||
m.chatMode = ChatModeSteering
|
||||
@@ -1211,6 +1221,20 @@ func (m Model) submit() (tea.Model, tea.Cmd) {
|
||||
m.beginIntent(m.workflows[m.wfIndex])
|
||||
return m, nil
|
||||
}
|
||||
// Launcher: a workflow is Tab-selected → start it with the typed text as its brief
|
||||
// (empty brief is allowed, like the intent flow). The server makes the session; we
|
||||
// focus it when its events arrive (pendingWorkflowFocus).
|
||||
if m.launcherWf > 0 && m.launcherWf <= len(m.workflows) {
|
||||
wf := m.workflows[m.launcherWf-1]
|
||||
m.recordInput(text)
|
||||
m.client.Send(protocol.StartSession(wf.ID, text))
|
||||
m.clearInput()
|
||||
m.inputMode = ModeRouter
|
||||
m.editMode = ModeNormal
|
||||
m.pendingWorkflowFocus = true
|
||||
m.launcherWf = 0
|
||||
return m, nil
|
||||
}
|
||||
// Entering an already-selected session (blank submit).
|
||||
if text == "" && m.selectedID != "" {
|
||||
m.sessionEntered = true
|
||||
|
||||
Reference in New Issue
Block a user