Skip to content

Commit 35ca165

Browse files
cpcloudclaude
andauthored
feat(config): add file_picker_dir setting for document file picker (#704)
## Summary - Add `documents.file_picker_dir` config option (TOML + `MICASA_FILE_PICKER_DIR` env var) to control the starting directory of the document file picker - Default to the system Downloads folder (`~/Downloads`) via `xdg.UserDirs.Download` for cross-platform support (Linux XDG, macOS, Windows) - Graceful fallback chain: configured dir → Downloads → cwd → `"."` closes #700 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d739ec4 commit 35ca165

File tree

6 files changed

+86
-7
lines changed

6 files changed

+86
-7
lines changed

cmd/micasa/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ func (cmd *runCmd) Run() error {
142142
}
143143

144144
opts := app.Options{
145-
DBPath: dbPath,
146-
ConfigPath: config.Path(),
145+
DBPath: dbPath,
146+
ConfigPath: config.Path(),
147+
FilePickerDir: cfg.Documents.ResolvedFilePickerDir(),
147148
}
148149

149150
chatCfg := cfg.LLM.ChatConfig()

internal/app/forms.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,11 +2416,15 @@ func (m *Model) newDocumentFilePicker(title string) *huh.FilePicker {
24162416
if h < 5 {
24172417
h = 5
24182418
}
2419-
// Resolve to absolute path so filepath.Dir can compute the real parent;
2420-
// the default "." stays stuck (filepath.Dir(".") == ".").
2421-
dir, err := os.Getwd()
2422-
if err != nil {
2423-
dir = "."
2419+
// Use the configured starting directory (defaults to ~/Downloads).
2420+
// Fall back to cwd if the configured dir is empty.
2421+
dir := m.filePickerDir
2422+
if dir == "" {
2423+
var err error
2424+
dir, err = os.Getwd()
2425+
if err != nil {
2426+
dir = "."
2427+
}
24242428
}
24252429
short := "\x1b[22m" + dimPath.Render("in "+shortenHome(dir))
24262430
return huh.NewFilePicker().

internal/app/model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ type Model struct {
180180
llmClient *llm.Client
181181
llmConfig *llmConfig // saved for extraction client creation
182182
llmExtraContext string // user-provided context appended to prompts
183+
filePickerDir string // starting directory for document file picker
183184
ex extractState
184185
pull pullState
185186
chat *chatState // non-nil when chat overlay is open
@@ -259,6 +260,7 @@ func NewModel(store *data.Store, options Options) (*Model, error) {
259260
llmClient: client,
260261
llmConfig: options.LLMConfig,
261262
llmExtraContext: extraContext,
263+
filePickerDir: options.FilePickerDir,
262264
ex: extractState{
263265
extractionProvider: options.ExtractionConfig.Provider,
264266
extractionBaseURL: options.ExtractionConfig.BaseURL,

internal/app/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ type detailContext struct {
226226
type Options struct {
227227
DBPath string
228228
ConfigPath string
229+
FilePickerDir string // starting directory for document file picker
229230
LLMConfig *llmConfig // nil if LLM is not configured
230231
ExtractionConfig extractionConfig
231232
}

internal/config/config.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,30 @@ type Documents struct {
212212
// CacheTTLDays is deprecated; use CacheTTL instead. Kept for backward
213213
// compatibility. Bare integer interpreted as days.
214214
CacheTTLDays *int `toml:"cache_ttl_days,omitempty" env:"MICASA_CACHE_TTL_DAYS"`
215+
216+
// FilePickerDir is the starting directory for the document file picker.
217+
// Default: the system Downloads folder (e.g. ~/Downloads).
218+
FilePickerDir string `toml:"file_picker_dir" env:"MICASA_FILE_PICKER_DIR"`
219+
}
220+
221+
// ResolvedFilePickerDir returns the starting directory for the file picker.
222+
// Uses the configured value if set and the directory exists, otherwise falls
223+
// back to the system Downloads folder, then the current working directory.
224+
func (d Documents) ResolvedFilePickerDir() string {
225+
if d.FilePickerDir != "" {
226+
if info, err := os.Stat(d.FilePickerDir); err == nil && info.IsDir() {
227+
return d.FilePickerDir
228+
}
229+
}
230+
if dir := xdg.UserDirs.Download; dir != "" {
231+
if info, err := os.Stat(dir); err == nil && info.IsDir() {
232+
return dir
233+
}
234+
}
235+
if dir, err := os.Getwd(); err == nil {
236+
return dir
237+
}
238+
return "."
215239
}
216240

217241
// CacheTTLDuration returns the resolved cache TTL as a time.Duration.
@@ -1012,6 +1036,10 @@ model = "` + DefaultModel + `"
10121036
# Default: 30d.
10131037
# cache_ttl = "30d"
10141038
1039+
# Starting directory for the document file picker.
1040+
# Default: system Downloads folder (~/Downloads on most systems).
1041+
# file_picker_dir = "/home/user/Documents"
1042+
10151043
[extraction]
10161044
# Timeout for pdftotext. Go duration syntax: "30s", "1m", etc. Default: "30s".
10171045
# Increase if you routinely process very large PDFs.

internal/config/config_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ func TestEnvVars(t *testing.T) {
623623
"MICASA_MAX_DOCUMENT_SIZE": "documents.max_file_size",
624624
"MICASA_CACHE_TTL": "documents.cache_ttl",
625625
"MICASA_CACHE_TTL_DAYS": "documents.cache_ttl_days",
626+
"MICASA_FILE_PICKER_DIR": "documents.file_picker_dir",
626627
"MICASA_EXTRACTION_MODEL": "extraction.model",
627628
"MICASA_MAX_EXTRACT_PAGES": "extraction.max_extract_pages",
628629
"MICASA_EXTRACTION_ENABLED": "extraction.enabled",
@@ -1120,3 +1121,45 @@ extra_context = "Portland house."
11201121
assert.Equal(t, chat.Thinking, ex.Thinking)
11211122
assert.Equal(t, chat.ExtraContext, ex.ExtraContext)
11221123
}
1124+
1125+
// --- FilePickerDir ---
1126+
1127+
func TestResolvedFilePickerDir_ConfiguredDirExists(t *testing.T) {
1128+
t.Parallel()
1129+
dir := t.TempDir()
1130+
d := Documents{FilePickerDir: dir}
1131+
assert.Equal(t, dir, d.ResolvedFilePickerDir())
1132+
}
1133+
1134+
func TestResolvedFilePickerDir_ConfiguredDirMissing(t *testing.T) {
1135+
t.Parallel()
1136+
d := Documents{FilePickerDir: "/nonexistent/path/that/does/not/exist"}
1137+
result := d.ResolvedFilePickerDir()
1138+
// Should fall back to Downloads or cwd, not the missing dir.
1139+
assert.NotEqual(t, "/nonexistent/path/that/does/not/exist", result)
1140+
assert.NotEmpty(t, result)
1141+
}
1142+
1143+
func TestResolvedFilePickerDir_EmptyFallsBackToDownloadsOrCwd(t *testing.T) {
1144+
t.Parallel()
1145+
d := Documents{}
1146+
result := d.ResolvedFilePickerDir()
1147+
assert.NotEmpty(t, result)
1148+
}
1149+
1150+
func TestFilePickerDir_FromTOML(t *testing.T) {
1151+
t.Parallel()
1152+
dir := t.TempDir()
1153+
path := writeConfig(t, "[documents]\nfile_picker_dir = '"+dir+"'\n")
1154+
cfg, err := LoadFromPath(path)
1155+
require.NoError(t, err)
1156+
assert.Equal(t, dir, cfg.Documents.FilePickerDir)
1157+
}
1158+
1159+
func TestFilePickerDir_FromEnv(t *testing.T) {
1160+
dir := t.TempDir()
1161+
t.Setenv("MICASA_FILE_PICKER_DIR", dir)
1162+
cfg, err := LoadFromPath(noConfig(t))
1163+
require.NoError(t, err)
1164+
assert.Equal(t, dir, cfg.Documents.FilePickerDir)
1165+
}

0 commit comments

Comments
 (0)