feat(tui): show session workspace (cwd) in the status bar
SessionWorkspaceBoundEvent already recorded the bound workspace root but it was never surfaced. Map it to a new session.workspace_bound frame (sealed ServerMessage variant, auto-registered; re-emitted on reconnect via the snapshot replay), carry it on Session.WorkspaceRoot, and show it home-abbreviated in the status bar (⌂ ~/Programs/correx). Golden test pins the decode.
This commit is contained in:
@@ -12,6 +12,7 @@ import com.correx.core.events.events.ArtifactCreatedEvent
|
|||||||
import com.correx.core.events.events.ChatSessionStartedEvent
|
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||||
import com.correx.core.events.events.ChatTurnEvent
|
import com.correx.core.events.events.ChatTurnEvent
|
||||||
import com.correx.core.events.events.InferenceCompletedEvent
|
import com.correx.core.events.events.InferenceCompletedEvent
|
||||||
|
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.events.InferenceStartedEvent
|
import com.correx.core.events.events.InferenceStartedEvent
|
||||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||||
@@ -69,6 +70,13 @@ suspend fun domainEventToServerMessage(
|
|||||||
sessionSequence = sessionSequence,
|
sessionSequence = sessionSequence,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is SessionWorkspaceBoundEvent -> ServerMessage.SessionWorkspaceBound(
|
||||||
|
sessionId = p.sessionId,
|
||||||
|
workspaceRoot = p.workspaceRoot,
|
||||||
|
sequence = seq,
|
||||||
|
sessionSequence = sessionSequence,
|
||||||
|
)
|
||||||
|
|
||||||
is ChatTurnEvent -> ServerMessage.ChatTurn(
|
is ChatTurnEvent -> ServerMessage.ChatTurn(
|
||||||
sessionId = p.sessionId,
|
sessionId = p.sessionId,
|
||||||
turnId = p.turnId,
|
turnId = p.turnId,
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ sealed interface ServerMessage {
|
|||||||
override val sessionSequence: Long,
|
override val sessionSequence: Long,
|
||||||
) : ServerMessage, SessionMessage
|
) : ServerMessage, SessionMessage
|
||||||
|
|
||||||
|
/** The workspace root a session was bound to, from SessionWorkspaceBoundEvent — shown as cwd. */
|
||||||
|
@Serializable
|
||||||
|
@SerialName("session.workspace_bound")
|
||||||
|
data class SessionWorkspaceBound(
|
||||||
|
val sessionId: SessionId,
|
||||||
|
val workspaceRoot: String,
|
||||||
|
override val sequence: Long,
|
||||||
|
override val sessionSequence: Long,
|
||||||
|
) : ServerMessage, SessionMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event-derived conversation turn (USER or ROUTER), from ChatTurnEvent. The client
|
* Event-derived conversation turn (USER or ROUTER), from ChatTurnEvent. The client
|
||||||
* rebuilds the chat transcript from these, ordered by [sessionSequence].
|
* rebuilds the chat transcript from these, ordered by [sessionSequence].
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func PreviewFrame(kind string, w, h int) string {
|
|||||||
m.sessionEntered = true
|
m.sessionEntered = true
|
||||||
if s := m.session("04a546aa"); s != nil {
|
if s := m.session("04a546aa"); s != nil {
|
||||||
s.CurrentStage = "write_script"
|
s.CurrentStage = "write_script"
|
||||||
|
s.WorkspaceRoot = "/home/kami/Programs/correx"
|
||||||
s.Events = sampleEvents()
|
s.Events = sampleEvents()
|
||||||
s.Pending = &Approval{
|
s.Pending = &Approval{
|
||||||
RequestID: "req-1", SessionID: "04a546aa", Tier: "T3", Risk: "MEDIUM",
|
RequestID: "req-1", SessionID: "04a546aa", Tier: "T3", Risk: "MEDIUM",
|
||||||
|
|||||||
@@ -98,19 +98,20 @@ type Approval struct {
|
|||||||
|
|
||||||
// Session is the UI's view of one server session.
|
// Session is the UI's view of one server session.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID string
|
ID string
|
||||||
Status string
|
Status string
|
||||||
WorkflowID string
|
WorkflowID string
|
||||||
Name string
|
Name string
|
||||||
LastEventAt int64
|
LastEventAt int64
|
||||||
CurrentStage string
|
CurrentStage string
|
||||||
LastOutput string
|
WorkspaceRoot string // bound cwd, from session.workspace_bound
|
||||||
LastResponse string
|
LastOutput string
|
||||||
Tools []ToolRecord
|
LastResponse string
|
||||||
ToolsByStage map[string][]ManifestTool
|
Tools []ToolRecord
|
||||||
Events []EventEntry
|
ToolsByStage map[string][]ManifestTool
|
||||||
Pending *Approval
|
Events []EventEntry
|
||||||
Active bool // an inference/tool is in flight (drives the spinner)
|
Pending *Approval
|
||||||
|
Active bool // an inference/tool is in flight (drives the spinner)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow is a launchable workflow advertised by the server.
|
// Workflow is a launchable workflow advertised by the server.
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
|||||||
if s := m.session(msg.SessionID); s != nil {
|
if s := m.session(msg.SessionID); s != nil {
|
||||||
s.addEvent(nowMillis(), "ArtifactCreated", msg.ArtifactID)
|
s.addEvent(nowMillis(), "ArtifactCreated", msg.ArtifactID)
|
||||||
}
|
}
|
||||||
|
case protocol.TypeWorkspaceBound:
|
||||||
|
if s := m.session(msg.SessionID); s != nil {
|
||||||
|
s.WorkspaceRoot = msg.WorkspaceRoot
|
||||||
|
}
|
||||||
case protocol.TypeApprovalRequired:
|
case protocol.TypeApprovalRequired:
|
||||||
m.onApprovalRequired(msg)
|
m.onApprovalRequired(msg)
|
||||||
case protocol.TypeApprovalResolved:
|
case protocol.TypeApprovalResolved:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -74,6 +75,9 @@ func (m Model) renderStatus() string {
|
|||||||
if s.Status != "" {
|
if s.Status != "" {
|
||||||
parts = append(parts, lipgloss.NewStyle().Foreground(statusColor(t, s.Status)).Background(bg).Render(statusLabel(s.Status)))
|
parts = append(parts, lipgloss.NewStyle().Foreground(statusColor(t, s.Status)).Background(bg).Render(statusLabel(s.Status)))
|
||||||
}
|
}
|
||||||
|
if s.WorkspaceRoot != "" {
|
||||||
|
parts = append(parts, span("⌂ "+shortPath(s.WorkspaceRoot), t.P.Faint))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model := m.currentModel
|
model := m.currentModel
|
||||||
@@ -391,6 +395,14 @@ func (m Model) eventRows(w, h int) []string {
|
|||||||
|
|
||||||
// --- width helpers ---
|
// --- width helpers ---
|
||||||
|
|
||||||
|
// shortPath abbreviates the user's home dir to ~ for a compact cwd in the status bar.
|
||||||
|
func shortPath(p string) string {
|
||||||
|
if home := os.Getenv("HOME"); home != "" && strings.HasPrefix(p, home) {
|
||||||
|
return "~" + p[len(home):]
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
// fill left-justifies a styled line and pads with bg to width w.
|
// fill left-justifies a styled line and pads with bg to width w.
|
||||||
func (m Model) fill(s string, w int, bg lipgloss.Color) string {
|
func (m Model) fill(s string, w int, bg lipgloss.Color) string {
|
||||||
return " " + padTo(s, w-1, bg)
|
return " " + padTo(s, w-1, bg)
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ func TestDecodeGoldenServerFrames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "session.workspace_bound carries the cwd",
|
||||||
|
json: `{"type":"session.workspace_bound","sessionId":"s1","workspaceRoot":"/home/kami/Programs/correx","sequence":4,"sessionSequence":1}`,
|
||||||
|
wantType: TypeWorkspaceBound,
|
||||||
|
eventBear: true,
|
||||||
|
check: func(t *testing.T, m ServerMessage) {
|
||||||
|
if m.WorkspaceRoot != "/home/kami/Programs/correx" {
|
||||||
|
t.Fatalf("workspaceRoot not decoded: %+v", m)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "snapshot_complete (non-event control frame)",
|
name: "snapshot_complete (non-event control frame)",
|
||||||
json: `{"type":"snapshot_complete"}`,
|
json: `{"type":"snapshot_complete"}`,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const (
|
|||||||
TypeToolRejected = "tool.rejected"
|
TypeToolRejected = "tool.rejected"
|
||||||
TypeApprovalRequired = "approval.required"
|
TypeApprovalRequired = "approval.required"
|
||||||
TypeApprovalResolved = "approval.resolved"
|
TypeApprovalResolved = "approval.resolved"
|
||||||
|
TypeWorkspaceBound = "session.workspace_bound"
|
||||||
TypeStageManifest = "stage.tool_manifest"
|
TypeStageManifest = "stage.tool_manifest"
|
||||||
TypeProviderStatus = "provider.status_changed"
|
TypeProviderStatus = "provider.status_changed"
|
||||||
TypeProtocolError = "protocol_error"
|
TypeProtocolError = "protocol_error"
|
||||||
@@ -52,25 +53,26 @@ const (
|
|||||||
type ServerMessage struct {
|
type ServerMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
SessionID string `json:"sessionId"`
|
SessionID string `json:"sessionId"`
|
||||||
WorkflowID string `json:"workflowId"`
|
WorkflowID string `json:"workflowId"`
|
||||||
StageID string `json:"stageId"`
|
StageID string `json:"stageId"`
|
||||||
ToolName string `json:"toolName"`
|
ToolName string `json:"toolName"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
TurnID string `json:"turnId"`
|
TurnID string `json:"turnId"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Summary string `json:"outputSummary"`
|
Summary string `json:"outputSummary"`
|
||||||
Response string `json:"responseText"`
|
Response string `json:"responseText"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Diff *string `json:"diff"`
|
Diff *string `json:"diff"`
|
||||||
Tier string `json:"tier"`
|
Tier string `json:"tier"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
RequestID string `json:"requestId"`
|
RequestID string `json:"requestId"`
|
||||||
ArtifactID string `json:"artifactId"`
|
ArtifactID string `json:"artifactId"`
|
||||||
ProviderID string `json:"providerId"`
|
ProviderID string `json:"providerId"`
|
||||||
Preview *string `json:"preview"`
|
Preview *string `json:"preview"`
|
||||||
Disposition string `json:"disposition"`
|
Disposition string `json:"disposition"`
|
||||||
Outcome string `json:"outcome"` // approval.resolved: APPROVED | REJECTED | AUTO_APPROVED
|
Outcome string `json:"outcome"` // approval.resolved: APPROVED | REJECTED | AUTO_APPROVED
|
||||||
|
WorkspaceRoot string `json:"workspaceRoot"` // session.workspace_bound: the bound cwd
|
||||||
|
|
||||||
SteeringEmitted bool `json:"steeringEmitted"`
|
SteeringEmitted bool `json:"steeringEmitted"`
|
||||||
OccurredAt int64 `json:"occurredAt"`
|
OccurredAt int64 `json:"occurredAt"`
|
||||||
@@ -180,7 +182,7 @@ func (m ServerMessage) IsEventBearing() bool {
|
|||||||
TypeInferenceStarted, TypeInferenceDone, TypeInferenceTimeout,
|
TypeInferenceStarted, TypeInferenceDone, TypeInferenceTimeout,
|
||||||
TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected,
|
TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected,
|
||||||
TypeToolAssessed,
|
TypeToolAssessed,
|
||||||
TypeApprovalRequired, TypeApprovalResolved, TypeArtifactCreated:
|
TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user