@@ -34,6 +34,7 @@ var wsTabs = []struct {
3434 label string
3535 view viewType
3636}{
37+ {"Settings" , viewWorkspaceSettings },
3738 {"Runs" , viewRuns },
3839 {"Variables" , viewVariables },
3940 {"Config Versions" , viewConfigVersions },
@@ -46,11 +47,12 @@ const (
4647 viewOrganizations viewType = iota // Phase 6: top-level org list (entry point)
4748 viewProjects
4849 viewWorkspaces
50+ viewWorkspaceSettings // Settings tab (first workspace sub-view tab)
4951 viewRuns
5052 viewVariables // Phase 5
5153 viewConfigVersions // Phase 5
5254 viewStateVersions // Phase 5
53- viewWorkspaceDetail // workspace settings detail (d key from workspace list)
55+ viewWorkspaceDetail // workspace detail (d key from workspace list or sub-views )
5456 viewOrgDetail // organization detail (d key from org list)
5557 viewProjectDetail // project detail (d key from project list)
5658 viewRunDetail // run detail (enter from run list) — Phase 7
@@ -143,8 +145,10 @@ type Model struct {
143145 svFiltering bool
144146
145147 // Workspace detail state
146- wsDetScroll int
147- wsDetPrevView viewType // view to return to when esc-ing from workspace detail
148+ wsDetScroll int
149+ wsDetPrevView viewType // view to return to when esc-ing from workspace detail
150+ wsSettingsScroll int // scroll offset for the Settings tab
151+ wsLatestChange * time.Time // latest-change-at from API; nil until loaded
148152
149153 // Organization detail state
150154 orgDetScroll int
@@ -273,6 +277,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
273277 case accountTokenLoadedMsg :
274278 m .accountToken = (* tfe .UserToken )(msg )
275279
280+ case wsLatestChangeMsg :
281+ m .wsLatestChange = (* time .Time )(msg )
282+
276283 case orgsLoadedMsg :
277284 m .orgs = []* tfe.Organization (msg )
278285 m .errMsg = ""
@@ -481,6 +488,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
481488 return m .handleProjectsKey (msg )
482489 case viewWorkspaces :
483490 return m .handleWorkspacesKey (msg )
491+ case viewWorkspaceSettings :
492+ return m .handleWorkspaceSettingsKey (msg )
484493 case viewRuns :
485494 return m .handleRunsKey (msg )
486495 case viewVariables :
@@ -575,7 +584,7 @@ func (m Model) navigateBack() (tea.Model, tea.Cmd) {
575584 m .wsCursor , m .wsOffset = 0 , 0
576585 m .wsFilter , m .wsFiltering = "" , false
577586 m .selectedProj = nil
578- case viewRuns , viewVariables , viewConfigVersions , viewStateVersions :
587+ case viewWorkspaceSettings , viewRuns , viewVariables , viewConfigVersions , viewStateVersions :
579588 // All workspace tab views return to the workspace list and clear sub-view data.
580589 m .currentView = viewWorkspaces
581590 m .runs = nil
@@ -722,10 +731,10 @@ func (m Model) refresh() (tea.Model, tea.Cmd) {
722731}
723732
724733// isWorkspaceSubView returns true when the current view is one of the
725- // workspace tab views (Runs, Variables, Config Versions, State Versions).
734+ // workspace tab views (Settings, Runs, Variables, Config Versions, State Versions).
726735func (m Model ) isWorkspaceSubView () bool {
727736 switch m .currentView {
728- case viewRuns , viewVariables , viewConfigVersions , viewStateVersions :
737+ case viewWorkspaceSettings , viewRuns , viewVariables , viewConfigVersions , viewStateVersions :
729738 return true
730739 }
731740 return false
@@ -738,6 +747,11 @@ func (m Model) switchWsTab(target viewType) (tea.Model, tea.Cmd) {
738747 m .errMsg = ""
739748
740749 switch target {
750+ case viewWorkspaceSettings :
751+ if m .wsLatestChange == nil && m .selectedWS != nil {
752+ return m , loadWorkspaceLatestChange (m .c , m .selectedWS .ID )
753+ }
754+ return m , nil
741755 case viewRuns :
742756 if m .runs != nil {
743757 return m , nil
@@ -1311,10 +1325,11 @@ func (m Model) handleWorkspacesKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
13111325 }
13121326 sel := filtered [m .wsCursor ]
13131327 m .selectedWS = sel
1314- m .loading = true
13151328 m .errMsg = ""
1316- m .runCursor , m .runOffset , m .runFilter = 0 , 0 , ""
1317- return m , tea .Batch (loadRuns (m .c , sel .ID ), tickSpinner ())
1329+ m .wsSettingsScroll = 0
1330+ m .wsLatestChange = nil
1331+ m .currentView = viewWorkspaceSettings
1332+ return m , loadWorkspaceLatestChange (m .c , sel .ID )
13181333 case "v" :
13191334 if n == 0 || m .wsCursor >= n {
13201335 break
@@ -1389,6 +1404,44 @@ func (m Model) handleWorkspaceDetailKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd
13891404 return m , nil
13901405}
13911406
1407+ func (m Model ) handleWorkspaceSettingsKey (msg tea.KeyPressMsg ) (tea.Model , tea.Cmd ) {
1408+ switch msg .String () {
1409+ case "up" , "k" :
1410+ if m .wsSettingsScroll > 0 {
1411+ m .wsSettingsScroll --
1412+ }
1413+ case "down" , "j" :
1414+ m .wsSettingsScroll ++
1415+ case "g" :
1416+ m .wsSettingsScroll = 0
1417+ case "G" :
1418+ m .wsSettingsScroll = 9999 // clamped to max by renderWorkspaceSettingsContent
1419+ case "left" :
1420+ // First tab — nothing to the left.
1421+ case "right" :
1422+ return m .switchWsTab (viewRuns )
1423+ case "u" :
1424+ if url := m .wsURL (); url != "" {
1425+ if err := copyToClipboard (url ); err == nil {
1426+ m .clipFeedback = "✓ workspace URL copied"
1427+ } else {
1428+ m .clipFeedback = "clipboard unavailable"
1429+ }
1430+ }
1431+ return m , nil
1432+ case "U" :
1433+ if url := m .wsURL (); url != "" {
1434+ if err := openBrowser (url ); err == nil {
1435+ m .clipFeedback = "✓ opening in browser"
1436+ } else {
1437+ m .clipFeedback = "could not open browser"
1438+ }
1439+ }
1440+ return m , nil
1441+ }
1442+ return m , nil
1443+ }
1444+
13921445func (m Model ) handleRunsKey (msg tea.KeyPressMsg ) (tea.Model , tea.Cmd ) {
13931446 filtered := filteredRuns (m )
13941447 n := len (filtered )
@@ -1421,7 +1474,7 @@ func (m Model) handleRunsKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
14211474 case "/" :
14221475 m .runFiltering = true
14231476 case "left" :
1424- // First tab — nothing to the left.
1477+ return m . switchWsTab ( viewWorkspaceSettings )
14251478 case "right" :
14261479 return m .switchWsTab (viewVariables )
14271480 case "enter" :
@@ -1965,6 +2018,8 @@ func (m Model) renderContent() string {
19652018 return m .renderProjectsContent ()
19662019 case viewWorkspaces :
19672020 return m .renderWorkspacesContent ()
2021+ case viewWorkspaceSettings :
2022+ return m .renderWorkspaceSettingsContent ()
19682023 case viewRuns :
19692024 return m .renderRunsContent ()
19702025 case viewVariables :
@@ -2284,6 +2339,11 @@ func (m Model) breadcrumbLine() string {
22842339 return orgPart + sep +
22852340 breadcrumbBarStyle .Render (fmt .Sprintf ("project: %s" , projName )) +
22862341 sep + breadcrumbActiveStyle .Render ("workspaces " )
2342+ case viewWorkspaceSettings :
2343+ return orgPart + sep +
2344+ breadcrumbBarStyle .Render (fmt .Sprintf ("project: %s" , projName )) +
2345+ sep + breadcrumbBarStyle .Render (fmt .Sprintf ("workspace: %s" , wsName )) +
2346+ sep + breadcrumbActiveStyle .Render ("settings " )
22872347 case viewRuns :
22882348 return orgPart + sep +
22892349 breadcrumbBarStyle .Render (fmt .Sprintf ("project: %s" , projName )) +
0 commit comments