-
Notifications
You must be signed in to change notification settings - Fork 0
Isolate CLI user state for Kai Dev #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,9 @@ import ( | |
| "encoding/xml" | ||
| "fmt" | ||
| "io" | ||
| "kai/internal/config" | ||
| "kai/internal/kitlauncher" | ||
| tuierrors "kai/internal/tui/errors" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
|
|
@@ -29,14 +32,8 @@ import ( | |
| "unicode" | ||
|
|
||
| "github.com/bmatcuk/doublestar/v4" | ||
| "github.com/mattn/go-isatty" | ||
| "github.com/sergi/go-diff/diffmatchpatch" | ||
| "github.com/spf13/cobra" | ||
| "gopkg.in/yaml.v3" | ||
|
|
||
| "github.com/kaicontext/kai-core/diff" | ||
| "github.com/kaicontext/kai-core/merge" | ||
|
|
||
| "github.com/kaicontext/kai-engine/ai" | ||
| "github.com/kaicontext/kai-engine/authorship" | ||
| "github.com/kaicontext/kai-engine/classify" | ||
|
|
@@ -64,9 +61,10 @@ import ( | |
| "github.com/kaicontext/kai-engine/telemetry" | ||
| "github.com/kaicontext/kai-engine/util" | ||
| "github.com/kaicontext/kai-engine/workspace" | ||
| "kai/internal/config" | ||
| "kai/internal/kitlauncher" | ||
| tuierrors "kai/internal/tui/errors" | ||
| "github.com/mattn/go-isatty" | ||
| "github.com/sergi/go-diff/diffmatchpatch" | ||
| "github.com/spf13/cobra" | ||
| "gopkg.in/yaml.v3" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -168,7 +166,7 @@ var verbose bool | |
| var authLoginToken string | ||
|
|
||
| // updateCheckFile is the path to the cached update check result. | ||
| var updateCheckFile = filepath.Join(os.Getenv("HOME"), ".kai", "update-check.json") | ||
| var updateCheckFile = kaipath.UserPath(os.Getenv("HOME"), "update-check.json") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updateCheckFile now captures KAI_DATA_DIR at package-init time, freezing the cache path for the process lifetime; runtime re-homing won't move it, and no test asserts it follows the env var. |
||
|
|
||
| type updateCheck struct { | ||
| LatestVersion string `json:"latest_version"` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -797,3 +797,24 @@ func TestResolveKitPath_EnvOverride(t *testing.T) { | |
| t.Fatal("a broken override must error, not fall back") | ||
| } | ||
| } | ||
|
|
||
| func TestDefaultDataDirectory(t *testing.T) { | ||
| home := t.TempDir() | ||
| t.Setenv("HOME", home) | ||
| t.Setenv("USERPROFILE", home) | ||
| t.Setenv("KAI_DATA_DIR", "") | ||
| t.Setenv("KAI_INSTALL_DIR", "") | ||
| if got := Default().BinDir; got != filepath.Join(home, ".kai", "bin") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the default-path leg passes identically on the old hardcoded code; only the KAI_DATA_DIR leg could distinguish old from new, and its power depends on the unread external kaipath.UserPath contract, so the regression is not verified. |
||
| t.Fatal(got) | ||
| } | ||
| dev := t.TempDir() | ||
| t.Setenv("KAI_DATA_DIR", dev) | ||
| if got := Default().BinDir; got != filepath.Join(dev, "bin") { | ||
| t.Fatal(got) | ||
| } | ||
| bundle := t.TempDir() | ||
| t.Setenv("KAI_INSTALL_DIR", bundle) | ||
| if got := Default().BinDir; got != bundle { | ||
| t.Fatalf("bundle override lost: %s", got) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,12 @@ package tui | |
| import ( | ||
| "context" | ||
| "fmt" | ||
| "kai/api/graph" | ||
| "kai/api/memstat" | ||
| "kai/api/projects" | ||
| "kai/api/provider" | ||
| "kai/api/watcher" | ||
| "kai/internal/tui/views" | ||
| "log" | ||
| "os" | ||
| "os/signal" | ||
|
|
@@ -22,13 +28,7 @@ import ( | |
|
|
||
| tea "github.com/charmbracelet/bubbletea" | ||
| "github.com/charmbracelet/lipgloss" | ||
|
|
||
| "kai/api/graph" | ||
| "kai/api/provider" | ||
| "kai/api/memstat" | ||
| "kai/api/projects" | ||
| "kai/internal/tui/views" | ||
| "kai/api/watcher" | ||
| "github.com/kaicontext/kai-engine/kaipath" | ||
| ) | ||
|
|
||
| // Options configures a TUI session. The TUI reads from a live graph | ||
|
|
@@ -589,10 +589,10 @@ type model struct { | |
| width int | ||
| height int | ||
|
|
||
| repl views.REPL | ||
| gate views.Gate | ||
| sync views.Sync | ||
| status views.StatusBar | ||
| repl views.REPL | ||
| gate views.Gate | ||
| sync views.Sync | ||
| status views.StatusBar | ||
| syncCh <-chan views.SyncEvent | ||
| chatCh <-chan views.ChatActivityEvent | ||
| hostProcCh <-chan views.HostProcEvent | ||
|
|
@@ -633,11 +633,11 @@ func initialModel(opts Options, syncCh <-chan views.SyncEvent, chatCh <-chan vie | |
| // non-primary roots in a multi-root workspace. | ||
| gate.SetProjects(opts.Projects) | ||
| return model{ | ||
| opts: opts, | ||
| repl: views.NewREPLWithSession(opts.Binary, opts.WorkDir, opts.Planner, opts.ResumeSessionID), | ||
| gate: gate, | ||
| sync: s, | ||
| status: status, | ||
| opts: opts, | ||
| repl: views.NewREPLWithSession(opts.Binary, opts.WorkDir, opts.Planner, opts.ResumeSessionID), | ||
| gate: gate, | ||
| sync: s, | ||
| status: status, | ||
| syncCh: syncCh, | ||
| chatCh: chatCh, | ||
| hostProcCh: hostProcCh, | ||
|
|
@@ -681,7 +681,7 @@ func (m model) Init() tea.Cmd { | |
| // short transient error line in the REPL so the user sees that | ||
| // SOMETHING went wrong without seeing the stack. | ||
| // | ||
| // Stack traces are written to ~/.kai/tui-panic.log so a developer | ||
| // Stack traces are written to the user Kai state directory (KAI_DATA_DIR or ~/.kai) so a developer | ||
| // can post-mortem without disturbing the user's session. | ||
| func (m model) Update(msg tea.Msg) (resultModel tea.Model, resultCmd tea.Cmd) { | ||
| defer func() { | ||
|
|
@@ -692,7 +692,7 @@ func (m model) Update(msg tea.Msg) (resultModel tea.Model, resultCmd tea.Cmd) { | |
| // state stays consistent; only the error display is | ||
| // added. | ||
| m.repl = m.repl.AppendSystemError(fmt.Sprintf( | ||
| "internal error suppressed (see ~/.kai/tui-panic.log) — continuing")) | ||
| "internal error suppressed (see the user Kai state directory (KAI_DATA_DIR or ~/.kai)) — continuing")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the user-facing message dropped the |
||
| resultModel = m | ||
| resultCmd = nil | ||
| } | ||
|
|
@@ -973,8 +973,7 @@ func (m *model) setFocus(f focus) { | |
| } | ||
| } | ||
|
|
||
|
|
||
| // logTUIPanic appends a stack trace to ~/.kai/tui-panic.log so a | ||
| // logTUIPanic appends a stack trace to the user Kai state directory (KAI_DATA_DIR or ~/.kai) so a | ||
| // developer can post-mortem the panic that just got swallowed by | ||
| // the recover in Update. Best-effort: failing to open the log | ||
| // must not itself panic. Falls back to UserHomeDir when the | ||
|
|
@@ -986,7 +985,7 @@ func logTUIPanic(m model, msg tea.Msg, panicVal any) { | |
| } | ||
| if dir == "" { | ||
| if home, err := os.UserHomeDir(); err == nil { | ||
| dir = filepath.Join(home, ".kai") | ||
| dir = kaipath.UserPath(home) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kaipath.UserPath(home) is the only zero-variadic call; whether it returns the base ~/.kai depends on the helper's contract, unverified, and if it assumes ≥1 component the panic-log fallback silently breaks.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kaipath.UserPath(home) is called with zero path elements; if the external contract requires a non-empty tail, the panic log silently lands in the wrong directory, and this call site has no test coverage. |
||
| } | ||
| } | ||
| if dir == "" { | ||
|
|
@@ -1005,7 +1004,6 @@ func logTUIPanic(m model, msg tea.Msg, panicVal any) { | |
| _, _ = f.Write(debug.Stack()) | ||
| } | ||
|
|
||
|
|
||
| // firstNonEmptyLine returns the first non-empty trimmed line of s, | ||
| // restoreTerminalForSafety emits the ANSI sequences that revert the | ||
| // modes Bubble Tea sets (alt-screen, mouse tracking, bracketed paste, | ||
|
|
@@ -1018,15 +1016,16 @@ func logTUIPanic(m model, msg tea.Msg, panicVal any) { | |
| // to stderr. | ||
| // | ||
| // Sequences: | ||
| // 1049l exit alternate screen buffer (return to the user's | ||
| // normal scrollback) | ||
| // 25h show cursor (Bubble Tea hides it during the run) | ||
| // 1000l disable basic mouse tracking | ||
| // 1002l disable cell-motion mouse tracking (what | ||
| // WithMouseCellMotion turned on) | ||
| // 1003l disable any-event mouse tracking (defensive) | ||
| // 2004l disable bracketed-paste mode | ||
| // ?7h re-enable line wrap (the default; some TUIs disable it) | ||
| // | ||
| // 1049l exit alternate screen buffer (return to the user's | ||
| // normal scrollback) | ||
| // 25h show cursor (Bubble Tea hides it during the run) | ||
| // 1000l disable basic mouse tracking | ||
| // 1002l disable cell-motion mouse tracking (what | ||
| // WithMouseCellMotion turned on) | ||
| // 1003l disable any-event mouse tracking (defensive) | ||
| // 2004l disable bracketed-paste mode | ||
| // ?7h re-enable line wrap (the default; some TUIs disable it) | ||
| func restoreTerminalForSafety() { | ||
| const reset = "\x1b[?1049l\x1b[?25h\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?2004l\x1b[?7h" | ||
| fmt.Fprint(os.Stderr, reset) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pre-existing os.Getenv("HOME") (unchanged by this PR) yields a relative path on Windows where HOME is unset; not this change's bug, just not fixed by it.