jeo-code 0.7.17 → 0.7.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/commands/launch.ts +81 -75
- package/src/skills/catalog.ts +23 -8
- package/src/tui/components/autocomplete.ts +79 -3
- package/src/tui/components/live-model-picker.ts +41 -1
- package/src/tui/components/select-list.ts +46 -3
- package/src/tui/components/slash.ts +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.7.19] - 2026-06-26
|
|
10
|
+
_The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`/model` live picker gets a provider/group tab bar (gajae-code parity).** `src/tui/components/select-list.ts` adds a synthetic `ALL_TAB` first tab plus one tab per distinct item `group`, a `tabList()`/`activeTab()`/`cycleTab()` API, and tab-scoped visibility in `computeVisible()`. `renderSelectList` gains a `showTabs` option that draws the tab bar (active tab highlighted) and a `tab provider` key hint; `renderLiveModelPicker` (`live-model-picker.ts`) enables it. `runLaunchCommand` (`src/commands/launch.ts`) wires the `tab` key (with `shift+tab` to step back) into the picker key handler.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Skills are invoked ONLY via the `$` entrypoint — declared aliases become `$`-invocable, never `/` commands.** This reverses 0.7.18's slash-alias dispatch: `src/skills/catalog.ts` replaces `parseSkillSlashInvocation` with `getSkillByDollarToken` (resolve by exact name → exact declared alias without its leading `/` → unique name prefix), and `parseSkillInvocation` now also matches a declared alias, so `$obsidian-capture` loads the skill that declares `/obsidian-capture`. `runLaunchCommand` drops every `/`-alias dispatch path (one-shot and REPL), leaves `skillSlashDetails` empty so the slash menu stays builtins-only, and surfaces declared-alias tokens in `$` autocomplete via `resolvedSkillTokens`.
|
|
17
|
+
|
|
18
|
+
## [0.7.18] - 2026-06-26
|
|
19
|
+
_Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget._
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Slash completion gains gjc §2.1 fuzzy name matching with a description fallback.** `src/tui/components/autocomplete.ts` adds pure `fuzzyMatch`/`fuzzyScore` (exact 100 > startsWith 80 > includes 60 > subsequence `40 − gaps×5`, min 1) and a `fuzzyCommandHits` ranker, so `/mdl` → `/model` now completes while prefix-exact behaviour (`/mod` → `/model`) is preserved. When nothing matches a command name, both the autocomplete dropdown and `matchSlash` (`slash.ts`) fall back to a description substring match — `/oauth` → `/login`, `/clipboard` → `/dump` — gated to ≥2-char queries with a real substring hit to avoid flooding. New `SLASH_COMMAND_DESCRIPTIONS` map backs the fallback.
|
|
23
|
+
- **Resolved skills can contribute `/` slash commands, not just `$<name>`.** Every slash alias a resolved skill declares (frontmatter `aliases:`/`slash:`) becomes a real, dispatchable palette command grouped under "skills" — extending the slash menu, autocomplete, and previews. `runLaunchCommand` (`src/commands/launch.ts`) wires `skillSlashAliases`/`parseSkillSlashInvocation` so both the interactive REPL (`/obsidian-capture note`) and one-shot mode (`jeo "/obsidian-capture note"`) dispatch the owning skill, with built-in slash handlers always winning and first-declarer-wins alias scoping.
|
|
24
|
+
- **`buildThinkingLevelChoices` extracts the reasoning menu (gjc `#renderThinkingMenu`).** `src/tui/components/live-model-picker.ts` adds `THINKING_LEVEL_ORDER` and a pure `buildThinkingLevelChoices(current, { inheritLabel, tokenHint })` that renders `<level> — <description> (<tokens>)` with the current level flagged and an optional leading `inherit` row for role targets — fully unit-testable.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **`/model` runs gjc's two-menu target → reasoning flow.** After a model is picked, `applyPickedModelWithTarget` now shows `applyTargetChoices` ("Set as DEFAULT / EXECUTOR / …", which target uses the model) followed by the per-target reasoning menu. DEFAULT updates the active session model and default thinking; a subagent role writes `~/.jeo/config.json` (model + thinking, or inherit) without switching the chat model. Replaces the prior default-only assignment.
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
- `bun run typecheck` clean; full suite green — 2009 pass / 0 fail across 234 files — including new `test/autocomplete.test.ts` cases (`fuzzyMatch` subsequence, `fuzzyScore` ranking, non-prefix subsequence completion, prefix-exact preservation, description-only fallback), `test/slash.test.ts` (`matchSlash` description fallback for intent queries), and `test/live-model-picker.test.ts` (`buildThinkingLevelChoices` ordering, token hints skipping minimal, `inherit` row only with a label, explicit role level outranking inherit).
|
|
31
|
+
|
|
9
32
|
|
|
10
33
|
## [0.7.17] - 2026-06-25
|
|
11
34
|
_Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact._
|
package/README.ja.md
CHANGED
|
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
205
205
|
## 変更履歴 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
|
|
209
|
+
- **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
|
|
208
210
|
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
209
211
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
210
212
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
211
|
-
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
212
|
-
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
205
205
|
## 변경 이력 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
|
|
209
|
+
- **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
|
|
208
210
|
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
209
211
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
210
212
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
211
|
-
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
212
|
-
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -232,11 +232,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
232
232
|
## Changelog
|
|
233
233
|
|
|
234
234
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
235
|
+
- **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
|
|
236
|
+
- **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
|
|
235
237
|
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
236
238
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
237
239
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
238
|
-
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
239
|
-
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
240
240
|
|
|
241
241
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
242
242
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
205
205
|
## 更新日志 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
|
|
209
|
+
- **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
|
|
208
210
|
- **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
|
|
209
211
|
- **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
|
|
210
212
|
- **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
|
|
211
|
-
- **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
|
|
212
|
-
- **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/commands/launch.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-
|
|
|
14
14
|
import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
15
15
|
import { runTeamEngine, type TeamEngineOptions } from "./team";
|
|
16
16
|
import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
17
|
-
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
17
|
+
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, skillSlashAliases, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
18
18
|
import { formatForgeBox, scaleForgeWidth } from "../tui/components/forge";
|
|
19
19
|
import { interactiveOAuthLogin } from "./auth";
|
|
20
20
|
import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
|
|
@@ -43,7 +43,7 @@ import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../ag
|
|
|
43
43
|
import { listAliases } from "../ai/model-registry";
|
|
44
44
|
import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
|
|
45
45
|
|
|
46
|
-
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting } from "../agent/subagents";
|
|
46
|
+
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting, applyTargetChoices } from "../agent/subagents";
|
|
47
47
|
import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
|
|
48
48
|
import { SessionPicker, renderSessionPicker } from "../tui/components/session-picker";
|
|
49
49
|
import {
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
formatPickListWithCapabilities,
|
|
58
58
|
formatCapabilityLine,
|
|
59
59
|
} from "../tui/components/config-panel";
|
|
60
|
-
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
60
|
+
import { liveModelPicker, renderLiveModelPicker, buildThinkingLevelChoices, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
61
61
|
|
|
62
62
|
import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
|
|
63
63
|
import { sanitizeForTerminal } from "../tui/components/code-view";
|
|
@@ -522,10 +522,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
522
522
|
|
|
523
523
|
const workflowSkills = workflowSkillsForPrompt(resolvedSkills);
|
|
524
524
|
const resolvedSkillNames = resolvedSkills.map(s => s.name);
|
|
525
|
-
// Skills are invoked ONLY via the
|
|
526
|
-
//
|
|
527
|
-
//
|
|
525
|
+
// Skills are invoked ONLY via the `$` entrypoint — never as `/` commands. A skill's declared
|
|
526
|
+
// aliases (frontmatter `aliases:`/`slash:`) are additionally `$`-invocable (`/obsidian-capture`
|
|
527
|
+
// → `$obsidian-capture`), so they extend `$` autocomplete (see resolvedSkillTokens) rather than
|
|
528
|
+
// the slash palette. The slash menu stays builtins-only, hence skillSlashDetails is empty.
|
|
528
529
|
const skillSlashDetails: SlashCommandInfo[] = [];
|
|
530
|
+
// Declared-alias tokens (the alias text without its leading `/`), surfaced in `$` autocomplete
|
|
531
|
+
// next to skill names so installing a skill grows the `$` menu, not the `/` menu.
|
|
532
|
+
const resolvedSkillTokens = [...new Set([
|
|
533
|
+
...resolvedSkillNames,
|
|
534
|
+
...resolvedSkills.flatMap(s => skillSlashAliases(s).map(a => a.replace(/^\//, ""))),
|
|
535
|
+
])];
|
|
529
536
|
|
|
530
537
|
const protocol = buildToolProtocol(allowedTools);
|
|
531
538
|
const preamble = flags.systemPrompt ?? "You are the jeo, an interactive coding agent.\nAccomplish the user's request by calling tools and verifying your work.";
|
|
@@ -1260,7 +1267,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1260
1267
|
}
|
|
1261
1268
|
return;
|
|
1262
1269
|
}
|
|
1263
|
-
//
|
|
1270
|
+
// A `$skill` invocation resolves to a single skill (by name, declared alias, or unique prefix).
|
|
1264
1271
|
const skillInvocation = parseSkillInvocation(messageContent, resolvedSkills);
|
|
1265
1272
|
if (skillInvocation) {
|
|
1266
1273
|
await runOneSkillShot(skillInvocation);
|
|
@@ -1460,7 +1467,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1460
1467
|
slashCommands: [...base.slashCommands, ...skillSlashDetails.map(d => d.command)],
|
|
1461
1468
|
liveModels: liveModelsCache ? flattenModels(liveModelsCache).map(e => e.model) : [],
|
|
1462
1469
|
aliases: aliasNames,
|
|
1463
|
-
skillNames:
|
|
1470
|
+
skillNames: resolvedSkillTokens,
|
|
1464
1471
|
modelsForProvider: p => liveModelsCache?.find(r => r.provider === p)?.models ?? [],
|
|
1465
1472
|
mentionPaths,
|
|
1466
1473
|
};
|
|
@@ -2213,7 +2220,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2213
2220
|
|
|
2214
2221
|
const runSelectPicker = async <T>(
|
|
2215
2222
|
render: (cols: number, rows: number) => string[],
|
|
2216
|
-
onKey: (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => boolean | undefined,
|
|
2223
|
+
onKey: (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean; shift?: boolean } | undefined) => boolean | undefined,
|
|
2217
2224
|
): Promise<void> => {
|
|
2218
2225
|
pickerActive = true; // closes the readline output gate for the picker's lifetime
|
|
2219
2226
|
disarmPreview();
|
|
@@ -2282,7 +2289,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2282
2289
|
process.stdout.on("resize", onPickerResize);
|
|
2283
2290
|
try {
|
|
2284
2291
|
await new Promise<void>(resolve => {
|
|
2285
|
-
const handler = (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
|
|
2292
|
+
const handler = (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean; shift?: boolean } | undefined) => {
|
|
2286
2293
|
const done = onKey(ch, key);
|
|
2287
2294
|
if (done) {
|
|
2288
2295
|
process.stdin.off("keypress", handler);
|
|
@@ -2397,6 +2404,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2397
2404
|
list.page(1, 6);
|
|
2398
2405
|
return false;
|
|
2399
2406
|
}
|
|
2407
|
+
if (key?.name === "tab") {
|
|
2408
|
+
// gajae-code parity: cycle the provider tab (shift+tab steps back).
|
|
2409
|
+
list.cycleTab(key.shift ? -1 : 1);
|
|
2410
|
+
return false;
|
|
2411
|
+
}
|
|
2400
2412
|
if (key?.name === "backspace") {
|
|
2401
2413
|
list.backspace();
|
|
2402
2414
|
return false;
|
|
@@ -2449,15 +2461,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2449
2461
|
current: ThinkLevel | undefined,
|
|
2450
2462
|
inheritLabel?: string,
|
|
2451
2463
|
): Promise<ThinkLevel | "inherit" | undefined> => {
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
{ value: "low", label: `low — light reasoning (~${Math.round(thinkingMaxTokens("low") / 1000)}k tokens)`, hint: mark("low") },
|
|
2457
|
-
{ value: "medium", label: `medium — moderate reasoning (~${Math.round(thinkingMaxTokens("medium") / 1000)}k tokens)`, hint: mark("medium") },
|
|
2458
|
-
{ value: "high", label: `high — deep reasoning (~${Math.round(thinkingMaxTokens("high") / 1000)}k tokens)`, hint: mark("high") },
|
|
2459
|
-
{ value: "xhigh", label: `xhigh — maximum reasoning (~${Math.round(thinkingMaxTokens("xhigh") / 1000)}k tokens)`, hint: mark("xhigh") },
|
|
2460
|
-
];
|
|
2464
|
+
const levels = buildThinkingLevelChoices(current, {
|
|
2465
|
+
inheritLabel,
|
|
2466
|
+
tokenHint: lvl => (lvl === "minimal" ? undefined : `~${Math.round(thinkingMaxTokens(lvl as ThinkLevel) / 1000)}k tokens`),
|
|
2467
|
+
});
|
|
2461
2468
|
const picked = await pickFromOptions(title, levels);
|
|
2462
2469
|
return picked === "inherit" || isThinkingLevel(picked) ? picked : undefined;
|
|
2463
2470
|
};
|
|
@@ -2485,57 +2492,63 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2485
2492
|
return leaf.replace(/^gpt/i, "GPT");
|
|
2486
2493
|
};
|
|
2487
2494
|
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
choices.push({ ...child, depth: 1, branch: index === children.length - 1 ? "last" : "mid" });
|
|
2495
|
-
});
|
|
2496
|
-
};
|
|
2497
|
-
|
|
2498
|
-
choices.push({
|
|
2499
|
-
value: "heading:default",
|
|
2500
|
-
label: "Set as DEFAULT (Default)",
|
|
2501
|
-
hint: `${config.defaultModel} (${currentDefaultThinking}) · roles → /agents`,
|
|
2502
|
-
disabled: true,
|
|
2503
|
-
});
|
|
2504
|
-
appendChildren([
|
|
2505
|
-
{ value: "default:keep", label: "Set model only", hint: `keep thinking ${currentDefaultThinking}` },
|
|
2506
|
-
...levels.map(level => ({
|
|
2507
|
-
value: `default:${level}`,
|
|
2508
|
-
label: `thinking ${level}`,
|
|
2509
|
-
hint: level === currentDefaultThinking ? "current" : `~${Math.round(thinkingMaxTokens(level) / 1000)}k tokens`,
|
|
2510
|
-
})),
|
|
2511
|
-
]);
|
|
2512
|
-
|
|
2513
|
-
return choices;
|
|
2514
|
-
};
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2495
|
+
// gajae-code `/model` parity: after a model is picked, the interactive flow
|
|
2496
|
+
// runs TWO menus — gjc's `#renderActionMenu` ("Set as DEFAULT / EXECUTOR / …",
|
|
2497
|
+
// i.e. WHICH target uses this model) followed by its `#renderThinkingMenu`
|
|
2498
|
+
// ("Reasoning for <target>", the per-target reasoning budget). DEFAULT updates
|
|
2499
|
+
// the active session model; a subagent role writes ~/.jeo/config.json without
|
|
2500
|
+
// switching the chat model.
|
|
2519
2501
|
const applyPickedModelWithTarget = async (target: string): Promise<boolean> => {
|
|
2520
2502
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
2521
2503
|
const cfgForPick = await readGlobalConfig();
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
await persistSessionModel();
|
|
2530
|
-
const defaultThinking = isThinkingLevel(action) ? action : undefined;
|
|
2531
|
-
if (defaultThinking) {
|
|
2532
|
-
sessionThinking = defaultThinking;
|
|
2504
|
+
const targetId = await pickFromOptions(
|
|
2505
|
+
`Model Name: ${displayModelName(target)}\n\nAction for: ${target}`,
|
|
2506
|
+
applyTargetChoices(cfgForPick).map(c => ({ value: c.value, label: c.label, hint: c.hint })),
|
|
2507
|
+
);
|
|
2508
|
+
if (!targetId) {
|
|
2509
|
+
console.log("(cancelled)");
|
|
2510
|
+
return true;
|
|
2533
2511
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2512
|
+
|
|
2513
|
+
if (targetId === "default") {
|
|
2514
|
+
const currentDefaultThinking = (sessionThinking ?? cfgForPick.thinkingLevel ?? "medium") as ThinkLevel;
|
|
2515
|
+
const level = await pickThinkingLevel(`Reasoning for default: ${target}`, currentDefaultThinking);
|
|
2516
|
+
if (level === undefined) {
|
|
2517
|
+
console.log("(cancelled)");
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
const { resolved, provider } = await describeModel(target);
|
|
2521
|
+
const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
|
|
2522
|
+
sessionModel = target;
|
|
2523
|
+
await persistSessionModel();
|
|
2524
|
+
const defaultThinking = isThinkingLevel(level) ? level : undefined;
|
|
2525
|
+
if (defaultThinking) {
|
|
2526
|
+
sessionThinking = defaultThinking;
|
|
2527
|
+
}
|
|
2528
|
+
await saveConfigPatch(raw => ({
|
|
2529
|
+
...rememberModelPatch(raw, target),
|
|
2530
|
+
...(defaultThinking ? { thinkingLevel: defaultThinking } : {}),
|
|
2531
|
+
}));
|
|
2532
|
+
console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default.`);
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const role = getSubagentRole(targetId, cfgForPick);
|
|
2537
|
+
if (!role) return false;
|
|
2538
|
+
const currentRoleThinking = resolveSubagentThinking(role.id, cfgForPick) as ThinkLevel | undefined;
|
|
2539
|
+
const level = await pickThinkingLevel(
|
|
2540
|
+
`Reasoning for ${role.title}: ${target}`,
|
|
2541
|
+
currentRoleThinking,
|
|
2542
|
+
`inherit (default ${cfgForPick.thinkingLevel ?? "medium"})`,
|
|
2543
|
+
);
|
|
2544
|
+
if (level === undefined) {
|
|
2545
|
+
console.log("(cancelled)");
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
const thinking = level === "inherit" ? undefined : (isThinkingLevel(level) ? level : undefined);
|
|
2549
|
+
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: target, thinking }) }));
|
|
2550
|
+
const { provider } = await describeModel(target);
|
|
2551
|
+
console.log(`${role.title} model set to ${target} (${provider})${level === "inherit" ? " · thinking inherits default" : ` · thinking ${level}`} — saved to ~/.jeo/config.json.`);
|
|
2539
2552
|
return true;
|
|
2540
2553
|
};
|
|
2541
2554
|
|
|
@@ -4383,15 +4396,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4383
4396
|
}
|
|
4384
4397
|
continue;
|
|
4385
4398
|
}
|
|
4386
|
-
|
|
4387
|
-
if (aliasInvocation?.invokedAs) {
|
|
4388
|
-
try {
|
|
4389
|
-
await runSkillInvocation(aliasInvocation.skill, aliasInvocation.intent, aliasInvocation.invokedAs);
|
|
4390
|
-
} catch (err) {
|
|
4391
|
-
console.log(`! ${(err as Error).message}`);
|
|
4392
|
-
}
|
|
4393
|
-
continue;
|
|
4394
|
-
}
|
|
4399
|
+
|
|
4395
4400
|
// Unresolved `$skill` → suggest precisely, never silently send the typo to the model.
|
|
4396
4401
|
// `$exact`/`$prefix`/chains already ran above; a leftover `$word` is a missed skill
|
|
4397
4402
|
// attempt, EXCEPT `$UPPERCASE` env-var-style tokens (e.g. `$HOME`) which pass through.
|
|
@@ -4419,6 +4424,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4419
4424
|
continue;
|
|
4420
4425
|
}
|
|
4421
4426
|
}
|
|
4427
|
+
|
|
4422
4428
|
// Unhandled slash attempt → suggest, don't send the typo to the model.
|
|
4423
4429
|
if (isSlashAttempt(input)) {
|
|
4424
4430
|
const m = matchSlash(input, [...completionContext().slashCommands]);
|
package/src/skills/catalog.ts
CHANGED
|
@@ -521,23 +521,38 @@ export function getSkillBySlash(skills: SkillDoc[], command: string): SkillDoc |
|
|
|
521
521
|
return skills.find(s => skillSlashAliases(s).some(a => a.toLowerCase() === q));
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/** Resolve a bare `$`-token (the text after `$`, no leading `$`) to a skill: exact name,
|
|
525
|
+
* then exact declared alias (the alias compared without its leading `/`), then unique name
|
|
526
|
+
* prefix. This makes declared aliases first-class `$` entrypoints — `$obsidian-capture`
|
|
527
|
+
* resolves the skill that declares `/obsidian-capture` — so skills stay `$`-managed and a
|
|
528
|
+
* declared `alias` becomes an invocable `$<alias>`, never a dispatchable `/` command. */
|
|
529
|
+
export function getSkillByDollarToken(skills: SkillDoc[], token: string): SkillDoc | undefined {
|
|
530
|
+
const q = token.toLowerCase();
|
|
531
|
+
if (!q) return undefined;
|
|
532
|
+
const byName = skills.find(s => s.name.toLowerCase() === q);
|
|
533
|
+
if (byName) return byName;
|
|
534
|
+
const byAlias = skills.find(s => skillSlashAliases(s).some(a => a.replace(/^\//, "").toLowerCase() === q));
|
|
535
|
+
if (byAlias) return byAlias;
|
|
536
|
+
return uniquePrefixSkill(skills, token);
|
|
537
|
+
}
|
|
538
|
+
|
|
524
539
|
export interface SkillInvocation {
|
|
525
540
|
skill: SkillDoc;
|
|
526
541
|
intent: string;
|
|
527
542
|
invokedAs?: string;
|
|
528
543
|
}
|
|
529
544
|
/** Parse only an explicit `$skill` invocation. Skills are invokable ONLY via the `$`
|
|
530
|
-
* entrypoint — `/` commands
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* any unknown `$word` falls through to the model as an ordinary prompt. */
|
|
545
|
+
* entrypoint — `/` commands never load a skill file, and pasted SKILL.md paths cannot
|
|
546
|
+
* hijack an ordinary coding request. Only the FIRST token counts, and only when a skill
|
|
547
|
+
* with that exact name, exact declared alias, or unique name prefix is loaded; `$HOME is
|
|
548
|
+
* what?` or any unknown `$word` falls through to the model as an ordinary prompt. */
|
|
534
549
|
export function parseSkillInvocation(input: string, skills: SkillDoc[]): SkillInvocation | null {
|
|
535
550
|
const trimmed = input.trim();
|
|
536
551
|
if (!trimmed) return null;
|
|
537
552
|
|
|
538
553
|
const command = trimmed.split(/\s+/, 1)[0] ?? "";
|
|
539
554
|
if (command.length > 1 && command.startsWith("$")) {
|
|
540
|
-
const dollarSkill =
|
|
555
|
+
const dollarSkill = getSkillByDollarToken(skills, command.slice(1));
|
|
541
556
|
if (dollarSkill) {
|
|
542
557
|
return { skill: dollarSkill, intent: trimmed.slice(command.length).trim(), invokedAs: command };
|
|
543
558
|
}
|
|
@@ -549,8 +564,8 @@ export function parseSkillInvocation(input: string, skills: SkillDoc[]): SkillIn
|
|
|
549
564
|
* "build auth". This is what lets `$` invoke several skills in one line — they all run,
|
|
550
565
|
* in order. Scanning stops at the first non-`$` token, OR a `$UPPERCASE` env-var-style
|
|
551
566
|
* token (e.g. `$HOME`), which is left in the intent so shell-style references pass through.
|
|
552
|
-
* Each `$name` resolves by exact name then unique prefix; names that
|
|
553
|
-
* into `unresolved` (so the REPL can report every typo, not just the first). Returns null
|
|
567
|
+
* Each `$name` resolves by exact name, exact declared alias, then unique prefix; names that
|
|
568
|
+
* resolve to nothing go into `unresolved` (so the REPL can report every typo, not just the first). Returns null
|
|
554
569
|
* only when the input opens with no parseable `$skill` token at all. */
|
|
555
570
|
export function parseSkillChain(
|
|
556
571
|
input: string,
|
|
@@ -567,7 +582,7 @@ export function parseSkillChain(
|
|
|
567
582
|
if (!tok.startsWith("$") || tok.length < 2) break;
|
|
568
583
|
const name = tok.slice(1);
|
|
569
584
|
if (/^[A-Z_][A-Z0-9_]*$/.test(name)) break; // env-var-style → boundary; keep in intent
|
|
570
|
-
const skill =
|
|
585
|
+
const skill = getSkillByDollarToken(skills, name);
|
|
571
586
|
if (skill) invocations.push({ skill, intent: "", invokedAs: tok });
|
|
572
587
|
else unresolved.push(name);
|
|
573
588
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* completer never blocks on the network. Static data (slash names, catalog ids,
|
|
14
14
|
* provider names, role ids) is filled by `staticCompletionContext()`.
|
|
15
15
|
*/
|
|
16
|
-
import { SLASH_COMMANDS } from "./slash";
|
|
16
|
+
import { SLASH_COMMANDS, SLASH_COMMAND_DESCRIPTIONS } from "./slash";
|
|
17
17
|
import { catalogIds } from "../../ai/model-catalog-compat";
|
|
18
18
|
import { PROVIDER_NAMES } from "../../ai/provider-status";
|
|
19
19
|
import { SUBAGENT_ROLES } from "../../agent/subagents";
|
|
@@ -84,6 +84,82 @@ function prefixHits(pool: string[], token: string): string[] {
|
|
|
84
84
|
return pool.filter(c => c.toLowerCase().startsWith(q));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Fuzzy subsequence test (gjc parity): every char of `query` appears in
|
|
89
|
+
* `target` in order (not necessarily adjacent). Empty query matches everything.
|
|
90
|
+
*/
|
|
91
|
+
export function fuzzyMatch(query: string, target: string): boolean {
|
|
92
|
+
if (!query) return true;
|
|
93
|
+
let qi = 0;
|
|
94
|
+
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
95
|
+
if (target[ti] === query[qi]) qi++;
|
|
96
|
+
}
|
|
97
|
+
return qi === query.length;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Score a match (gjc parity): exact=100 > startsWith=80 > includes=60 >
|
|
102
|
+
* subsequence(40 − gaps×5, min 1). Returns 0 when `query` is not a subsequence.
|
|
103
|
+
*/
|
|
104
|
+
export function fuzzyScore(query: string, target: string): number {
|
|
105
|
+
if (!query) return 80;
|
|
106
|
+
if (query === target) return 100;
|
|
107
|
+
if (target.startsWith(query)) return 80;
|
|
108
|
+
if (target.includes(query)) return 60;
|
|
109
|
+
let qi = 0;
|
|
110
|
+
let gaps = 0;
|
|
111
|
+
let lastMatch = -1;
|
|
112
|
+
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
113
|
+
if (target[ti] === query[qi]) {
|
|
114
|
+
if (lastMatch !== -1 && ti !== lastMatch + 1) gaps++;
|
|
115
|
+
lastMatch = ti;
|
|
116
|
+
qi++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (qi < query.length) return 0;
|
|
120
|
+
return Math.max(1, 40 - gaps * 5);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fuzzy command-name matches, ranked best-first (gjc §2.1): higher fuzzyScore
|
|
125
|
+
* first, ties broken by registration order so the list stays stable. Prefix
|
|
126
|
+
* matches always outrank looser subsequence hits, so `/mod`→`/model` is kept
|
|
127
|
+
* while `/mdl`→`/model` now also completes.
|
|
128
|
+
*/
|
|
129
|
+
function fuzzyHits(pool: string[], token: string): string[] {
|
|
130
|
+
const q = token.toLowerCase();
|
|
131
|
+
return pool
|
|
132
|
+
.map((c, index) => ({ c, score: fuzzyScore(q, c.toLowerCase()), index }))
|
|
133
|
+
.filter(x => x.score > 0)
|
|
134
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
135
|
+
.map(x => x.c);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Command-name completion with a description fallback (gjc §2.1). Name matches
|
|
140
|
+
* win outright (so `/mod`→`/model` stays precise); only when NOTHING matches the
|
|
141
|
+
* name do we fall back to substring-or-better description hits, so intent-style
|
|
142
|
+
* queries with no literal name match still resolve — e.g. `/oauth`→`/login`,
|
|
143
|
+
* `/transcript`→`/dump`. The description fallback requires ≥2 query chars and a
|
|
144
|
+
* real substring (not a loose subsequence) to avoid flooding the dropdown.
|
|
145
|
+
*/
|
|
146
|
+
function fuzzyCommandHits(pool: string[], token: string): string[] {
|
|
147
|
+
const nameHits = fuzzyHits(pool, token);
|
|
148
|
+
if (nameHits.length > 0) return nameHits;
|
|
149
|
+
|
|
150
|
+
const q = token.replace(/^\//, "").toLowerCase();
|
|
151
|
+
if (q.length < 2) return [];
|
|
152
|
+
return pool
|
|
153
|
+
.map((c, index) => {
|
|
154
|
+
const desc = SLASH_COMMAND_DESCRIPTIONS.get(c);
|
|
155
|
+
const hit = desc?.includes(q) ?? false;
|
|
156
|
+
return { c, score: hit ? fuzzyScore(q, desc!) * 0.5 : 0, index };
|
|
157
|
+
})
|
|
158
|
+
.filter(x => x.score > 0)
|
|
159
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
160
|
+
.map(x => x.c);
|
|
161
|
+
}
|
|
162
|
+
|
|
87
163
|
/** De-duplicate (case-insensitive, first wins), preserving order, capped. */
|
|
88
164
|
function dedupeCap(items: string[], cap = MAX_COMPLETIONS): string[] {
|
|
89
165
|
const seen = new Set<string>();
|
|
@@ -129,7 +205,7 @@ export function complete(line: string, ctx: CompletionContext): CompletionResult
|
|
|
129
205
|
// `/command` mention completion mid-line (the leading-token case is the
|
|
130
206
|
// dedicated command branch below, which also completes arguments).
|
|
131
207
|
if (token.startsWith("/")) {
|
|
132
|
-
return { completions: dedupeCap(
|
|
208
|
+
return { completions: dedupeCap(fuzzyCommandHits(ctx.slashCommands, token)), token, kind: "command" };
|
|
133
209
|
}
|
|
134
210
|
return { completions: [], token: line, kind: "none" };
|
|
135
211
|
}
|
|
@@ -137,7 +213,7 @@ export function complete(line: string, ctx: CompletionContext): CompletionResult
|
|
|
137
213
|
// Completing the command name itself (single token, still typing it).
|
|
138
214
|
if (tokens.length <= 1 && !trailingSpace) {
|
|
139
215
|
const token = tokens[0] ?? "/";
|
|
140
|
-
return { completions: dedupeCap(
|
|
216
|
+
return { completions: dedupeCap(fuzzyCommandHits(ctx.slashCommands, token)), token, kind: "command" };
|
|
141
217
|
}
|
|
142
218
|
|
|
143
219
|
const cmd = tokens[0]!.toLowerCase();
|
|
@@ -104,5 +104,45 @@ export function liveModelPicker(entries: PickEntry[], opts: LiveModelPickerOptio
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function renderLiveModelPicker(list: SelectList<PickEntry>, opts: RenderSelectOptions = {}): string[] {
|
|
107
|
-
return renderSelectList(list, { title: "Select a live model", rows: 12, ...opts });
|
|
107
|
+
return renderSelectList(list, { title: "Select a live model", rows: 12, showTabs: true, ...opts });
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/** The five reasoning levels in display order, lightest → heaviest. */
|
|
111
|
+
export const THINKING_LEVEL_ORDER = ["minimal", "low", "medium", "high", "xhigh"] as const;
|
|
112
|
+
|
|
113
|
+
const THINKING_LEVEL_DESCRIPTION: Record<string, string> = {
|
|
114
|
+
minimal: "lightest reasoning",
|
|
115
|
+
low: "light reasoning",
|
|
116
|
+
medium: "moderate reasoning",
|
|
117
|
+
high: "deep reasoning",
|
|
118
|
+
xhigh: "maximum reasoning",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export interface ThinkingLevelChoiceOptions {
|
|
122
|
+
/** Prepend an "inherit" row (role targets only, which can follow the default). */
|
|
123
|
+
inheritLabel?: string;
|
|
124
|
+
/** Per-level reasoning-budget hint, e.g. "~32k tokens". Return undefined to omit. */
|
|
125
|
+
tokenHint?: (level: string) => string | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* gajae-code-parity reasoning menu (gjc's `#renderThinkingMenu`): the per-target
|
|
130
|
+
* "Reasoning for …" choices shown after a model is picked. Each level renders as
|
|
131
|
+
* `<level> — <description> (<tokens>)` with the current level flagged. Role
|
|
132
|
+
* targets gain a leading `inherit` row. Pure — fully unit-testable.
|
|
133
|
+
*/
|
|
134
|
+
export function buildThinkingLevelChoices(
|
|
135
|
+
current: string | undefined,
|
|
136
|
+
opts: ThinkingLevelChoiceOptions = {},
|
|
137
|
+
): SelectItem<string>[] {
|
|
138
|
+
const items: SelectItem<string>[] = [];
|
|
139
|
+
if (opts.inheritLabel) {
|
|
140
|
+
items.push({ value: "inherit", label: opts.inheritLabel, hint: current === undefined ? "current" : "" });
|
|
141
|
+
}
|
|
142
|
+
for (const level of THINKING_LEVEL_ORDER) {
|
|
143
|
+
const tokens = opts.tokenHint?.(level);
|
|
144
|
+
const label = `${level} — ${THINKING_LEVEL_DESCRIPTION[level]}${tokens ? ` (${tokens})` : ""}`;
|
|
145
|
+
items.push({ value: level, label, hint: current === level ? "current" : "" });
|
|
146
|
+
}
|
|
147
|
+
return items;
|
|
148
|
+
}
|
|
@@ -12,6 +12,9 @@ import { padLineTo } from "./layout";
|
|
|
12
12
|
import { visibleWidth } from "./color";
|
|
13
13
|
import { truncate as truncateAnsi } from "../terminal";
|
|
14
14
|
|
|
15
|
+
/** Synthetic first tab that shows every item regardless of its provider/group. */
|
|
16
|
+
export const ALL_TAB = "ALL";
|
|
17
|
+
|
|
15
18
|
export interface SelectItem<T> {
|
|
16
19
|
value: T;
|
|
17
20
|
label: string;
|
|
@@ -32,6 +35,7 @@ export interface SelectItem<T> {
|
|
|
32
35
|
export class SelectList<T> {
|
|
33
36
|
private readonly items: SelectItem<T>[];
|
|
34
37
|
private query = "";
|
|
38
|
+
private tabIndex = 0; // 0 = ALL; 1..N = a provider/group tab
|
|
35
39
|
private cursor = 0; // index into the *visible* list
|
|
36
40
|
|
|
37
41
|
constructor(items: SelectItem<T>[]) {
|
|
@@ -45,9 +49,35 @@ export class SelectList<T> {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
private computeVisible(): SelectItem<T>[] {
|
|
52
|
+
let list = this.items;
|
|
53
|
+
const tab = this.activeTab();
|
|
54
|
+
if (tab !== ALL_TAB) list = list.filter(i => (i.group ?? "") === tab);
|
|
48
55
|
const q = this.query.trim().toLowerCase();
|
|
49
|
-
if (!q) return
|
|
50
|
-
return
|
|
56
|
+
if (!q) return list;
|
|
57
|
+
return list.filter(i => i.label.toLowerCase().includes(q) || (i.group ?? "").toLowerCase().includes(q));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Distinct provider/group tabs: "ALL" first, then each group in first-seen order. */
|
|
61
|
+
tabList(): string[] {
|
|
62
|
+
const seen: string[] = [];
|
|
63
|
+
for (const it of this.items) {
|
|
64
|
+
if (it.group && !seen.includes(it.group)) seen.push(it.group);
|
|
65
|
+
}
|
|
66
|
+
return [ALL_TAB, ...seen];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Active tab — "ALL" shows everything; otherwise items of one provider/group. */
|
|
70
|
+
activeTab(): string {
|
|
71
|
+
const tabs = this.tabList();
|
|
72
|
+
return tabs[Math.min(this.tabIndex, tabs.length - 1)] ?? ALL_TAB;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Cycle the active provider/group tab (wraps); resets the cursor to the first match. */
|
|
76
|
+
cycleTab(dir: 1 | -1): void {
|
|
77
|
+
const n = this.tabList().length;
|
|
78
|
+
if (n <= 1) return;
|
|
79
|
+
this.tabIndex = (Math.min(this.tabIndex, n - 1) + dir + n) % n;
|
|
80
|
+
this.cursor = this.firstEnabled(this.computeVisible());
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
private firstEnabled(list: SelectItem<T>[]): number {
|
|
@@ -136,6 +166,8 @@ export interface RenderSelectOptions {
|
|
|
136
166
|
unicode?: boolean;
|
|
137
167
|
/** Apply chalk color (default true). */
|
|
138
168
|
color?: boolean;
|
|
169
|
+
/** Render a provider/group tab bar (gajae-code `/model` parity) and the `tab` key hint. */
|
|
170
|
+
showTabs?: boolean;
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
/**
|
|
@@ -159,6 +191,15 @@ export function renderSelectList<T>(list: SelectList<T>, opts: RenderSelectOptio
|
|
|
159
191
|
}
|
|
160
192
|
}
|
|
161
193
|
|
|
194
|
+
if (opts.showTabs) {
|
|
195
|
+
const tabs = list.tabList();
|
|
196
|
+
if (tabs.length > 1) {
|
|
197
|
+
const active = list.activeTab();
|
|
198
|
+
const bar = " " + tabs.map(t => (t === active ? tint(`[${t}]`, chalk.cyan.bold) : tint(` ${t} `, chalk.gray))).join(" ");
|
|
199
|
+
out.push(cols ? clampToCols(bar, cols) : bar);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
162
203
|
const items = list.visible();
|
|
163
204
|
if (items.length === 0) {
|
|
164
205
|
const empty = tint(" (no matches)", chalk.gray);
|
|
@@ -208,7 +249,9 @@ export function renderSelectList<T>(list: SelectList<T>, opts: RenderSelectOptio
|
|
|
208
249
|
|
|
209
250
|
const q = list.filter();
|
|
210
251
|
const filterPart = q ? `filter: ${q}` : "type to filter";
|
|
211
|
-
const
|
|
252
|
+
const showTab = opts.showTabs && list.tabList().length > 1;
|
|
253
|
+
const tabHint = showTab ? (unicode ? " \u00b7 tab provider" : " . tab provider") : "";
|
|
254
|
+
const keys = (unicode ? "\u2191/\u2193 move \u00b7 enter select \u00b7 esc cancel" : "up/down move . enter select . esc cancel") + tabHint;
|
|
212
255
|
const footer = tint(` ${filterPart} \u2014 ${keys}`, chalk.gray);
|
|
213
256
|
out.push(cols ? clampToCols(footer, cols) : footer);
|
|
214
257
|
return out;
|
|
@@ -65,6 +65,11 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
65
65
|
|
|
66
66
|
export const SLASH_COMMANDS = SLASH_COMMAND_DETAILS.map(c => c.command);
|
|
67
67
|
|
|
68
|
+
/** Bundled command name → lowercased description (gjc §2.1 description matching). */
|
|
69
|
+
export const SLASH_COMMAND_DESCRIPTIONS = new Map<string, string>(
|
|
70
|
+
SLASH_COMMAND_DETAILS.map(c => [c.command, c.description.toLowerCase()]),
|
|
71
|
+
);
|
|
72
|
+
|
|
68
73
|
export function mergeSlashCommandDetails(extra: readonly SlashCommandInfo[] = []): SlashCommandInfo[] {
|
|
69
74
|
const byCommand = new Map<string, SlashCommandInfo>();
|
|
70
75
|
for (const d of [...SLASH_COMMAND_DETAILS, ...extra]) byCommand.set(d.command, d);
|
|
@@ -73,8 +78,10 @@ export function mergeSlashCommandDetails(extra: readonly SlashCommandInfo[] = []
|
|
|
73
78
|
|
|
74
79
|
/** Return the slash commands that match `input` (case-insensitive). Prefix matches
|
|
75
80
|
* come first; a fuzzy subsequence fallback (gjc-style, e.g. `/expt` → `/export`)
|
|
76
|
-
* follows so typos / abbreviations still surface a command.
|
|
77
|
-
*
|
|
81
|
+
* follows so typos / abbreviations still surface a command. When NOTHING matches
|
|
82
|
+
* the name, fall back to a description substring match (gjc §2.1) so intent-style
|
|
83
|
+
* queries with no literal name match still resolve — e.g. `/oauth` → `/login`,
|
|
84
|
+
* `/clipboard` → `/dump`. A bare `/` lists every command. Empty for non-slash input. */
|
|
78
85
|
export function matchSlash(input: string, commands: string[] = SLASH_COMMANDS): string[] {
|
|
79
86
|
if (!input.startsWith("/")) return [];
|
|
80
87
|
const q = input.toLowerCase();
|
|
@@ -82,7 +89,12 @@ export function matchSlash(input: string, commands: string[] = SLASH_COMMANDS):
|
|
|
82
89
|
if (body === "") return [...commands];
|
|
83
90
|
const starts = commands.filter(c => c.toLowerCase().startsWith(q));
|
|
84
91
|
const fuzzy = commands.filter(c => !starts.includes(c) && subsequence(body, c.slice(1).toLowerCase()));
|
|
85
|
-
|
|
92
|
+
const named = [...starts, ...fuzzy];
|
|
93
|
+
if (named.length > 0) return named;
|
|
94
|
+
// Description fallback: only when no name matches, and only for queries of ≥2
|
|
95
|
+
// chars with a real substring hit (not a loose subsequence), to avoid flooding.
|
|
96
|
+
if (body.length < 2) return [];
|
|
97
|
+
return commands.filter(c => SLASH_COMMAND_DESCRIPTIONS.get(c)?.includes(body) ?? false);
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
/** True when `input` looks like a slash command (starts with "/" and has no space). */
|