Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dist/

# Development tools
.task/
.gocache/
.gomodcache/

# Private documentation
docs/VISION.md
Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ tasks:
cmds:
- go generate ./internal/powersync
- go generate ./internal/sqlite
- sed '/^-- Auto-generated/d; /^-- Run .task generate/d' ../control-plane/internal/powersync/schema.sql > internal/chat/tools/query_schema.sql
- sed '/^-- Auto-generated/d; /^-- Run .task generate/d' ../control-plane/internal/infra/powersync/schema.sql > internal/app/chattools/query_schema.sql

# ===========================================================================
# Build
Expand Down
1 change: 0 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ func (m *Model) newChat() *chat.Model {
return chat.New(
m.user,
m.account,
m.workspace,
m.theme,
m.db,
m.runtimeDeps,
Expand Down
2 changes: 1 addition & 1 deletion internal/app/chat/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func newTestChat(t *testing.T, client chat.Client) *Model {
db := dbtest.OpenTestDB(t)
runtimeDeps := usecase.NewRuntimeDeps(db, client)

m := New(nil, domain.Account{ID: "acct-1"}, domain.Workspace{ID: "ws-1"}, theme, db, runtimeDeps, nil, scope)
m := New(nil, domain.Account{ID: "acct-1"}, theme, db, runtimeDeps, nil, scope)
m.SetSize(80, 40)
return m
}
Expand Down
2 changes: 0 additions & 2 deletions internal/app/chat/empty_state_poll_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ func TestEmptyStatePollDoesNotMutateSynchronously(t *testing.T) {
m := New(
nil,
domain.Account{ID: "acct-1"},
domain.Workspace{ID: "ws-1"},
styles.NewTheme(true),
mockDB,
usecase.RuntimeDeps{},
Expand Down Expand Up @@ -58,7 +57,6 @@ func TestEmptyStateSummaryMessageUpdatesState(t *testing.T) {
m := New(
nil,
domain.Account{ID: "acct-1"},
domain.Workspace{ID: "ws-1"},
styles.NewTheme(true),
mockDB,
usecase.RuntimeDeps{},
Expand Down
1 change: 0 additions & 1 deletion internal/app/chat/input_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ func (m *Model) createConversation(input msgs.UserSubmittedInput) tea.Cmd {
convID, err := m.db.Conversations().Create(
ctx,
m.account.ID,
m.workspace.ID,
)
if err != nil {
m.scope.Error("failed to create conversation", "error", err)
Expand Down
17 changes: 7 additions & 10 deletions internal/app/chat/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,13 @@ type Model struct {
conversationID domain.ConversationID
session *corechat.Session

user *auth.User
account domain.Account
workspace domain.Workspace
theme styles.Theme
width int
height int
originX int
originY int
user *auth.User
account domain.Account
theme styles.Theme
width int
height int
originX int
originY int

// Empty state
policySummary *domain.AccountSummary
Expand All @@ -88,7 +87,6 @@ type emptyStateSummaryLoadedMsg struct {
func New(
user *auth.User,
account domain.Account,
workspace domain.Workspace,
theme styles.Theme,
db sqlite.DB,
runtimeDeps usecase.RuntimeDeps,
Expand All @@ -103,7 +101,6 @@ func New(
messageList: messagelist.New(theme, runtimeDeps, toolRegistry, scope),
user: user,
account: account,
workspace: workspace,
theme: theme,
db: db,
runtimeDeps: runtimeDeps,
Expand Down
797 changes: 492 additions & 305 deletions internal/app/chattools/query_schema.sql

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions internal/app/onboarding/gate_requirements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ func TestRewindGateFor(t *testing.T) {
{name: "datadog api rewinds to region when site missing", target: bootstrap.GateDatadogAPIKey, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: bootstrap.GateDatadogRegion},
{name: "datadog app rewinds to api key when api key missing", target: bootstrap.GateDatadogAppKey, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1"), DDSite: "US1"}, want: bootstrap.GateDatadogAPIKey},
{name: "discovery rewinds to datadog check without dd account", target: bootstrap.GateDatadogDiscovery, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: bootstrap.GateDatadogCheck},
{name: "sync rewinds to workspace", target: bootstrap.GateSync, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: bootstrap.GateWorkspaceSelect},
{name: "sync stays when requirements met", target: bootstrap.GateSync, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1"), Workspace: ptrWorkspace("ws-1")}, want: bootstrap.GateSync},
{name: "sync stays when account is selected", target: bootstrap.GateSync, state: bootstrap.State{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: bootstrap.GateSync},
}

for _, tc := range tests {
Expand Down
7 changes: 6 additions & 1 deletion internal/app/onboarding/preflight/preflight_effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import (

tea "charm.land/bubbletea/v2"

iauth "github.com/usetero/cli/internal/auth"
"github.com/usetero/cli/internal/core/bootstrap"
"github.com/usetero/cli/internal/domain"
)

func (m *Model) checkAuth() tea.Cmd {
return func() tea.Msg {
hasValidAuth := false
var user *iauth.User
if m.auth.IsAuthenticated() {
if _, err := m.auth.GetAccessToken(m.ctx); err == nil {
hasValidAuth = true
if userID, err := m.auth.GetUserID(m.ctx); err == nil && userID != "" {
user = &iauth.User{ID: userID}
}
} else {
_ = m.auth.ClearTokens()
}
}
return preflightAuthCheckCompletedMsg{hasValidAuth: hasValidAuth}
return preflightAuthCheckCompletedMsg{hasValidAuth: hasValidAuth, user: user}
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/app/onboarding/preflight/preflight_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package preflight

import (
"github.com/usetero/cli/internal/auth"
"github.com/usetero/cli/internal/core/bootstrap"
"github.com/usetero/cli/internal/domain"
)
Expand All @@ -11,6 +12,7 @@ type preflightResolutionCompletedMsg struct {

type preflightAuthCheckCompletedMsg struct {
hasValidAuth bool
user *auth.User
}

type preflightOrganizationsLoadedMsg struct {
Expand Down
1 change: 1 addition & 0 deletions internal/app/onboarding/preflight/preflight_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {

func (m *Model) handleAuthChecked(msg preflightAuthCheckCompletedMsg) tea.Cmd {
m.state.HasValidAuth = msg.hasValidAuth
m.state.User = msg.user
if !m.state.HasValidAuth {
return m.emitResult()
}
Expand Down
8 changes: 3 additions & 5 deletions internal/app/onboarding/transition_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ func (m *Model) commandForTransition(event bootstrap.Event, transition bootstrap
m.scope.Info("onboarding complete",
slog.String("org_id", transition.Completion.Org.ID.String()),
slog.String("account_id", transition.Completion.Account.ID.String()),
slog.String("workspace_id", string(transition.Completion.Workspace.ID)),
)
return func() tea.Msg {
return bootstrap.OnboardingComplete{
User: transition.Completion.User,
Org: transition.Completion.Org,
Account: transition.Completion.Account,
Workspace: transition.Completion.Workspace,
User: transition.Completion.User,
Org: transition.Completion.Org,
Account: transition.Completion.Account,
}
}
case bootstrap.TransitionNoop:
Expand Down
30 changes: 25 additions & 5 deletions internal/app/onboarding/transitions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ func TestHandleTransitionPreflightRouting(t *testing.T) {
}
}

func TestHandleTransitionPreflightResolvedCarriesUserState(t *testing.T) {
t.Parallel()

m := newTestModel(t)
user := ptrUser("user-1")

cmd := m.handleTransition(bootstrap.PreflightResolved{State: bootstrap.PreflightState{
HasValidAuth: true,
User: user,
Role: bootstrap.RolePlatform,
Org: ptrOrg("org-1"),
Account: ptrAccount("acc-1"),
}})
if cmd == nil {
t.Fatal("expected command")
}
if m.state.User == nil || m.state.User.ID != user.ID {
t.Fatalf("user state = %+v, want %s", m.state.User, user.ID)
}
}

func TestHandleTransitionDatadogBranchRouting(t *testing.T) {
t.Parallel()

Expand All @@ -107,9 +128,9 @@ func TestHandleTransitionDatadogBranchRouting(t *testing.T) {
msg any
wantGate Gate
}{
{name: "datadog ready goes to workspace select", msg: bootstrap.DatadogReady{}, wantGate: bootstrap.GateWorkspaceSelect},
{name: "datadog ready goes to sync", msg: bootstrap.DatadogReady{}, wantGate: bootstrap.GateSync},
{name: "datadog needed goes to region", msg: bootstrap.DatadogNeeded{}, wantGate: bootstrap.GateDatadogRegion},
{name: "discovery complete goes to workspace select", msg: bootstrap.DatadogDiscoveryComplete{}, wantGate: bootstrap.GateWorkspaceSelect},
{name: "discovery complete goes to sync", msg: bootstrap.DatadogDiscoveryComplete{}, wantGate: bootstrap.GateSync},
}

for _, tc := range tests {
Expand Down Expand Up @@ -233,7 +254,6 @@ func TestHandleTransitionSyncComplete(t *testing.T) {
m.state.User = ptrUser("user-1")
m.state.Org = ptrOrg("org-1")
m.state.Account = ptrAccount("acc-1")
m.state.Workspace = ptrWorkspace("ws-1")

cmd := m.handleTransition(bootstrap.SyncComplete{})
if cmd == nil {
Expand All @@ -244,7 +264,7 @@ func TestHandleTransitionSyncComplete(t *testing.T) {
if !ok {
t.Fatalf("message type = %T, want bootstrap.OnboardingComplete", msg)
}
if complete.Org.ID != "org-1" || complete.Account.ID != "acc-1" || complete.Workspace.ID != "ws-1" || complete.User.ID != "user-1" {
if complete.Org.ID != "org-1" || complete.Account.ID != "acc-1" || complete.User.ID != "user-1" {
t.Fatalf("unexpected completion payload: %+v", complete)
}
}
Expand All @@ -255,7 +275,7 @@ func TestHandleTransitionSyncCompleteMissingStateNoops(t *testing.T) {
m := newTestModel(t)
m.state.User = ptrUser("user-1")
m.state.Org = ptrOrg("org-1")
// Missing account/workspace should not panic or emit completion payload.
// Missing account should not panic or emit completion payload.

cmd := m.handleTransition(bootstrap.SyncComplete{})
if cmd != nil {
Expand Down
2 changes: 0 additions & 2 deletions internal/app/onboarding_orchestration.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ func (m *Model) handleOnboardingMessage(msg tea.Msg) (tea.Cmd, bool) {
m.state = stateChat
m.user = msg.User
m.account = msg.Account
m.workspace = msg.Workspace
m.scope.Info("onboarding complete",
"org", msg.Org.Name,
"account", msg.Account.Name,
"workspace", msg.Workspace.Name,
)

// Create chat model (sizing happens via updateLayout)
Expand Down
21 changes: 13 additions & 8 deletions internal/boundary/graphql/conversation_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (

// CreateConversationInput contains the fields for creating a conversation.
type CreateConversationInput struct {
ID uuid.UUID
ID uuid.UUID
AccountID domain.AccountID
// WorkspaceID is kept only for legacy server compatibility.
WorkspaceID domain.WorkspaceID
Title string
}
Expand Down Expand Up @@ -50,12 +52,16 @@ func NewConversationService(client Client, scope log.Scope) *ConversationService

// Create creates a new conversation with the given client-provided ID.
func (s *ConversationService) Create(ctx context.Context, input CreateConversationInput) (*domain.Conversation, error) {
s.scope.Debug("creating conversation via API", "id", input.ID.String(), "workspaceID", input.WorkspaceID.String(), "title", input.Title)
s.scope.Debug("creating conversation via API", "id", input.ID.String(), "accountID", input.AccountID.String(), "workspaceID", input.WorkspaceID.String(), "title", input.Title)

genInput := gen.CreateConversationInput{
Id: ptr(input.ID.String()),
WorkspaceID: input.WorkspaceID.String(),
Title: ptr(input.Title),
Id: ptr(input.ID.String()),
Title: ptr(input.Title),
}
if input.AccountID != "" {
genInput.AccountID = ptr(input.AccountID.String())
} else if input.WorkspaceID != "" {
genInput.WorkspaceID = ptr(input.WorkspaceID.String())
}

resp, err := s.client.CreateConversation(ctx, genInput)
Expand All @@ -68,9 +74,8 @@ func (s *ConversationService) Create(ctx context.Context, input CreateConversati
}

conversation := &domain.Conversation{
ID: domain.ConversationID(resp.CreateConversation.Id),
WorkspaceID: input.WorkspaceID,
Title: input.Title,
ID: domain.ConversationID(resp.CreateConversation.Id),
Title: input.Title,
}

s.scope.Debug("created conversation via API", "id", conversation.ID)
Expand Down
Loading
Loading