claudeup 4.18.0 → 4.22.0
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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +92 -0
- package/src/__tests__/alias-shell-writer.test.ts +7 -0
- package/src/__tests__/alias-store.test.ts +77 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +118 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/alias-flags.ts +11 -1
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/alias-shell-writer.ts +382 -8
- package/src/services/alias-store.ts +60 -0
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +16 -11
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/screens/AliasScreen.tsx +491 -359
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/PluginsScreen.tsx +3 -1
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +6 -102
- package/src/ui/state/types.ts +8 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -196
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -764
- package/src/services/alias-store.js +0 -77
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -444
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -346
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1008
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -766
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -543
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -2,7 +2,8 @@ import React, { useEffect, useState, useCallback, useMemo, useRef } from "react"
|
|
|
2
2
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
3
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
4
4
|
import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
|
|
5
|
-
import { ScreenLayout } from "../components/layout/index.js";
|
|
5
|
+
import { ScreenLayout, type FooterHint } from "../components/layout/index.js";
|
|
6
|
+
import { FlagDetailEditor } from "../components/FlagDetailEditor.js";
|
|
6
7
|
import {
|
|
7
8
|
ALIAS_FLAGS,
|
|
8
9
|
FLAG_GROUPS,
|
|
@@ -12,6 +13,9 @@ import {
|
|
|
12
13
|
defaultAliasConfig,
|
|
13
14
|
defaultValueFor,
|
|
14
15
|
validateAliasName,
|
|
16
|
+
derivedChannelValues,
|
|
17
|
+
withoutDerivedChannel,
|
|
18
|
+
CHANNELS_FLAG_ID,
|
|
15
19
|
DEFAULT_ALIAS_NAME,
|
|
16
20
|
type AliasConfig,
|
|
17
21
|
type FlagValue,
|
|
@@ -20,12 +24,14 @@ import { loadAliasName, saveAliasName } from "../../services/alias-settings.js";
|
|
|
20
24
|
import {
|
|
21
25
|
detectShells,
|
|
22
26
|
renderArgs,
|
|
23
|
-
renderAlias,
|
|
24
27
|
validateConfig,
|
|
25
28
|
writeAliasToShell,
|
|
26
29
|
parseAliasFromRc,
|
|
30
|
+
findAdoptableAlias,
|
|
27
31
|
type ShellTarget,
|
|
28
32
|
type FlagValidationIssue,
|
|
33
|
+
type Segment,
|
|
34
|
+
type AdoptableAlias,
|
|
29
35
|
} from "../../services/alias-shell-writer.js";
|
|
30
36
|
import { readFile } from "node:fs/promises";
|
|
31
37
|
import { existsSync } from "node:fs";
|
|
@@ -36,6 +42,26 @@ interface FlagRow {
|
|
|
36
42
|
value: FlagValue;
|
|
37
43
|
/** Pre-computed validation issue for this row, if any. */
|
|
38
44
|
issue?: FlagValidationIssue;
|
|
45
|
+
/**
|
|
46
|
+
* Override for the checkbox's enabled state when the rendered alias would
|
|
47
|
+
* carry this flag even though its OWN value is disabled. Used by --channels:
|
|
48
|
+
* a dev-loaded channel forces `--channels` to emit (the render-time union),
|
|
49
|
+
* so the row must show `[x]` to match what gets written — even when the
|
|
50
|
+
* user's own channels list is off. Undefined → use the flag's own enabled.
|
|
51
|
+
*/
|
|
52
|
+
effectiveEnabled?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Override for the trailing detail (e.g. the --channels union count: own +
|
|
55
|
+
* derived). Undefined → derive from the flag's own value. Empty string is a
|
|
56
|
+
* valid override meaning "show nothing".
|
|
57
|
+
*/
|
|
58
|
+
effectiveDetail?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Override for the detail-pane `State:` line. For --channels this is the
|
|
61
|
+
* effective channel-list summary (the union of own + derived), so the detail
|
|
62
|
+
* agrees with the list count and the Preview. Undefined → summarizeValue.
|
|
63
|
+
*/
|
|
64
|
+
stateSummary?: string;
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
interface AliasNameRow {
|
|
@@ -78,22 +104,6 @@ function isSelectableItem(item: Item): boolean {
|
|
|
78
104
|
*/
|
|
79
105
|
type ViewMode = { kind: "main" } | { kind: "flag-detail"; flagId: string };
|
|
80
106
|
|
|
81
|
-
/**
|
|
82
|
-
* Items shown in the flag-detail sub-screen. Distinct from `Item` because the
|
|
83
|
-
* navigation context is different (one flag, not the whole catalog).
|
|
84
|
-
*/
|
|
85
|
-
type DetailItem =
|
|
86
|
-
| { kind: "toggle"; enabled: boolean }
|
|
87
|
-
| { kind: "value"; index: number; text: string }
|
|
88
|
-
| { kind: "picklist"; token: string; picked: boolean }
|
|
89
|
-
| { kind: "custom"; index: number; text: string }
|
|
90
|
-
| { kind: "add" }
|
|
91
|
-
| { kind: "header"; label: string };
|
|
92
|
-
|
|
93
|
-
function isSelectableDetailItem(item: DetailItem): boolean {
|
|
94
|
-
return item.kind !== "header";
|
|
95
|
-
}
|
|
96
|
-
|
|
97
107
|
export function AliasScreen() {
|
|
98
108
|
const { state } = useApp();
|
|
99
109
|
const modal = useModal();
|
|
@@ -103,12 +113,118 @@ export function AliasScreen() {
|
|
|
103
113
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
104
114
|
const [error, setError] = useState<string | null>(null);
|
|
105
115
|
const [viewMode, setViewMode] = useState<ViewMode>({ kind: "main" });
|
|
106
|
-
const [detailSelectedIdx, setDetailSelectedIdx] = useState(0);
|
|
107
116
|
// Snapshot of the rc file's flag values as parsed on mount / write. Used
|
|
108
117
|
// to compute the dirty indicator: in-memory != rcSnapshot ⇒ unsaved.
|
|
109
118
|
const [rcSnapshot, setRcSnapshot] = useState<Record<string, FlagValue> | null>(
|
|
110
119
|
null,
|
|
111
120
|
);
|
|
121
|
+
// Transient write feedback shown on the header status line (not a blocking
|
|
122
|
+
// modal). Auto-clears after a few seconds. Errors still use a modal since
|
|
123
|
+
// they need to be read and acknowledged.
|
|
124
|
+
const [writeStatus, setWriteStatus] = useState<string | null>(null);
|
|
125
|
+
|
|
126
|
+
// A hand-written `claude` alias found OUTSIDE any managed block on mount.
|
|
127
|
+
// Set when the rc file has no managed block but a stray alias we could
|
|
128
|
+
// adopt — drives the one-time "import it?" offer below. Cleared once the
|
|
129
|
+
// offer has been shown (whether accepted or declined).
|
|
130
|
+
const [adoptable, setAdoptable] = useState<AdoptableAlias | null>(null);
|
|
131
|
+
// When the user accepts a LOSSLESS adoption, we remember the line range of
|
|
132
|
+
// their original alias so the next write replaces it in place (no duplicate
|
|
133
|
+
// alias). Cleared after that write absorbs it into the managed block.
|
|
134
|
+
const [pendingAdoptRange, setPendingAdoptRange] = useState<
|
|
135
|
+
[number, number] | null
|
|
136
|
+
>(null);
|
|
137
|
+
|
|
138
|
+
// Clear the write status line a few seconds after it appears.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!writeStatus) return;
|
|
141
|
+
const t = setTimeout(() => setWriteStatus(null), 4000);
|
|
142
|
+
return () => clearTimeout(t);
|
|
143
|
+
}, [writeStatus]);
|
|
144
|
+
|
|
145
|
+
// One-time "adopt a hand-written alias?" offer. Fires when the mount load
|
|
146
|
+
// found a stray `claude` alias outside any managed block. We import only the
|
|
147
|
+
// flags we recognized; whether we also TAKE OWNERSHIP (remove the original
|
|
148
|
+
// line on the next write) depends on `lossless`:
|
|
149
|
+
// - lossless: every token round-trips → safe to absorb. We stash the line
|
|
150
|
+
// range so the next write replaces it in place (no duplicate alias).
|
|
151
|
+
// - not lossless: we couldn't parse some tokens (e.g. an uncatalogued
|
|
152
|
+
// flag like --model) → import the known flags for convenience but LEAVE
|
|
153
|
+
// the original line untouched. CRITICAL: we must NOT adopt its name here.
|
|
154
|
+
// Shell alias resolution is last-definition-wins, so a managed block
|
|
155
|
+
// sharing the name would shadow the surviving original at `source` time —
|
|
156
|
+
// silently killing the very tokens that made it not-lossless. Keeping the
|
|
157
|
+
// tool's own name lets both coexist: the original keeps working, the
|
|
158
|
+
// managed block lives under a different name.
|
|
159
|
+
// True once we've shown the offer. A ref (not state) so it survives re-render
|
|
160
|
+
// without being a dependency — critical because this effect mutates its own
|
|
161
|
+
// `adoptable` dep, and tying "fire once" to state/deps would let the dep
|
|
162
|
+
// change abort the in-flight modal mid-await (the cleanup would fire before
|
|
163
|
+
// the user answered). The ref decouples one-shot from the dependency array.
|
|
164
|
+
const didOfferAdoptRef = useRef(false);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!adoptable || !config || didOfferAdoptRef.current) return;
|
|
167
|
+
didOfferAdoptRef.current = true;
|
|
168
|
+
(async () => {
|
|
169
|
+
const a = adoptable;
|
|
170
|
+
|
|
171
|
+
const body = a.rawLine.trim();
|
|
172
|
+
const others =
|
|
173
|
+
a.others.length > 0
|
|
174
|
+
? `\n\n(Also found ${a.others.length} other claude alias${
|
|
175
|
+
a.others.length === 1 ? "" : "es"
|
|
176
|
+
} — leaving those alone.)`
|
|
177
|
+
: "";
|
|
178
|
+
|
|
179
|
+
// On a not-lossless adopt the managed block must NOT share the original's
|
|
180
|
+
// name — same name + last-wins shell resolution = the block shadows the
|
|
181
|
+
// surviving original, killing its uncatalogued tokens. Compute a managed
|
|
182
|
+
// name distinct from `a.name`: keep the tool's current name if it already
|
|
183
|
+
// differs, else derive a suffixed variant that's free in the rc file.
|
|
184
|
+
const managedName = a.lossless
|
|
185
|
+
? a.name
|
|
186
|
+
: pickDistinctName(config.aliasName, a.name, a.others);
|
|
187
|
+
|
|
188
|
+
const message = a.lossless
|
|
189
|
+
? `Found a hand-written alias:\n\n ${body}\n\nImport its flags into the editor? On the next write it'll be absorbed into a managed block (your original line is replaced — no duplicate).${others}`
|
|
190
|
+
: `Found a hand-written alias:\n\n ${body}\n\nI can import the flags I recognize, but it uses ${
|
|
191
|
+
a.leftovers.length
|
|
192
|
+
} token${a.leftovers.length === 1 ? "" : "s"} I don't manage (${a.leftovers
|
|
193
|
+
.map((t) => JSON.stringify(t))
|
|
194
|
+
.join(", ")}). I'll import the known flags and LEAVE your original "${a.name}" line untouched so it keeps working. The managed alias will use the name "${managedName}" — distinct from "${a.name}" so it can't override it. Import the known flags?${others}`;
|
|
195
|
+
|
|
196
|
+
const accepted = await modal.confirm("Adopt existing alias?", message);
|
|
197
|
+
if (accepted) {
|
|
198
|
+
// Lossless: adopt the name AND take ownership (replace the line on
|
|
199
|
+
// write). Not-lossless: import flags under a distinct managed name and
|
|
200
|
+
// never touch the original line — so it survives AND keeps resolving.
|
|
201
|
+
setConfig((prev) =>
|
|
202
|
+
prev
|
|
203
|
+
? {
|
|
204
|
+
...prev,
|
|
205
|
+
aliasName: managedName,
|
|
206
|
+
flags: { ...prev.flags, ...a.flags },
|
|
207
|
+
}
|
|
208
|
+
: prev,
|
|
209
|
+
);
|
|
210
|
+
if (a.lossless) {
|
|
211
|
+
// Persist the adopted name and stash the line range so the next
|
|
212
|
+
// write replaces the original in place (no duplicate, no shadow).
|
|
213
|
+
void persistAliasName(a.name);
|
|
214
|
+
setPendingAdoptRange(a.lineRange);
|
|
215
|
+
} else {
|
|
216
|
+
// Persist the distinct managed name so it survives a reopen.
|
|
217
|
+
void persistAliasName(managedName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Clear the candidate now that the decision is made — after the await,
|
|
221
|
+
// never before (clearing before would abort this very async via the
|
|
222
|
+
// dep-change cleanup, the bug this effect previously had).
|
|
223
|
+
setAdoptable(null);
|
|
224
|
+
})();
|
|
225
|
+
// persistAliasName is stable (useCallback); excluded to keep this one-shot.
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [adoptable, config, modal]);
|
|
112
228
|
|
|
113
229
|
// ── Load alias name + parse rc file + detect shells on mount ─────────
|
|
114
230
|
// The alias name is computer-wide and persisted in ~/.claude/settings.json.
|
|
@@ -132,6 +248,7 @@ export function AliasScreen() {
|
|
|
132
248
|
base.aliasName = name;
|
|
133
249
|
|
|
134
250
|
let parsedFlags: Record<string, FlagValue> | null = null;
|
|
251
|
+
let foundAdoptable: AdoptableAlias | null = null;
|
|
135
252
|
if (readTarget && existsSync(readTarget.path)) {
|
|
136
253
|
try {
|
|
137
254
|
const rcText = await readFile(readTarget.path, "utf8");
|
|
@@ -142,6 +259,11 @@ export function AliasScreen() {
|
|
|
142
259
|
// Honour an existing alias name from the rc if settings is at default.
|
|
143
260
|
// But the user's stored preference wins when set explicitly.
|
|
144
261
|
// (No-op here — `name` already loaded above.)
|
|
262
|
+
} else {
|
|
263
|
+
// No managed block on disk. The user may still have a hand-written
|
|
264
|
+
// `claude` alias elsewhere — detect it so we can offer to adopt it
|
|
265
|
+
// instead of looking broken. Non-destructive: just a candidate.
|
|
266
|
+
foundAdoptable = findAdoptableAlias(rcText);
|
|
145
267
|
}
|
|
146
268
|
} catch {
|
|
147
269
|
// rc unreadable — fall back to defaults; not a hard error.
|
|
@@ -149,9 +271,14 @@ export function AliasScreen() {
|
|
|
149
271
|
}
|
|
150
272
|
|
|
151
273
|
if (cancelled) return;
|
|
274
|
+
// No mirror normalization: channels.values holds OWN channels only;
|
|
275
|
+
// derived channels come from the dev-load flag at display/render time.
|
|
276
|
+
// The parser already subtracts derived values from the parsed channels
|
|
277
|
+
// list, so `base.flags` is correct as-is.
|
|
152
278
|
setConfig(base);
|
|
153
279
|
setShells(s);
|
|
154
|
-
setRcSnapshot(parsedFlags ?? base.flags);
|
|
280
|
+
setRcSnapshot(parsedFlags ?? { ...base.flags });
|
|
281
|
+
if (foundAdoptable) setAdoptable(foundAdoptable);
|
|
155
282
|
} catch (err) {
|
|
156
283
|
if (cancelled) return;
|
|
157
284
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -183,6 +310,31 @@ export function AliasScreen() {
|
|
|
183
310
|
if (groupFlags.length === 0) continue;
|
|
184
311
|
out.push({ kind: "header", label: group.label });
|
|
185
312
|
for (const flag of groupFlags) {
|
|
313
|
+
// --channels shows enabled when the rendered alias would emit it —
|
|
314
|
+
// i.e. own channels are on OR a dev-loaded channel is derived in. This
|
|
315
|
+
// keeps the checkbox honest about what gets written without mutating
|
|
316
|
+
// the own-channels enabled flag (which would silently re-emit own
|
|
317
|
+
// values the user disabled).
|
|
318
|
+
let effectiveEnabled: boolean | undefined;
|
|
319
|
+
let effectiveDetail: string | undefined;
|
|
320
|
+
let stateSummary: string | undefined;
|
|
321
|
+
if (flag.id === CHANNELS_FLAG_ID) {
|
|
322
|
+
const own = config.flags[flag.id];
|
|
323
|
+
const ownValues =
|
|
324
|
+
own && own.kind === "text-list" && own.enabled
|
|
325
|
+
? own.values.filter((v) => v.length > 0)
|
|
326
|
+
: [];
|
|
327
|
+
const derived = derivedChannelValues(config.flags);
|
|
328
|
+
// Deduped union — matches the writer's render-time union and the
|
|
329
|
+
// detail-pane summary, so the count is consistent everywhere.
|
|
330
|
+
const total = new Set([...ownValues, ...derived]).size;
|
|
331
|
+
effectiveEnabled = total > 0;
|
|
332
|
+
// Show the union count so the row matches what's written. When the
|
|
333
|
+
// count is purely derived, this reads e.g. `[x] --channels 1`.
|
|
334
|
+
effectiveDetail = total > 0 ? `${total}` : "";
|
|
335
|
+
// The detail-pane State line shows the same final channel list.
|
|
336
|
+
stateSummary = channelsStateSummary(config.flags);
|
|
337
|
+
}
|
|
186
338
|
out.push({
|
|
187
339
|
kind: "flag",
|
|
188
340
|
row: {
|
|
@@ -190,6 +342,9 @@ export function AliasScreen() {
|
|
|
190
342
|
flag,
|
|
191
343
|
value: config.flags[flag.id] ?? defaultValueFor(flag),
|
|
192
344
|
issue: issueMap.get(flag.id),
|
|
345
|
+
effectiveEnabled,
|
|
346
|
+
effectiveDetail,
|
|
347
|
+
stateSummary,
|
|
193
348
|
},
|
|
194
349
|
});
|
|
195
350
|
}
|
|
@@ -206,8 +361,9 @@ export function AliasScreen() {
|
|
|
206
361
|
|
|
207
362
|
// ── Flag-detail sub-screen items ──────────────────────────────────────
|
|
208
363
|
// Computed only when viewMode is flag-detail. Built off the flag's current
|
|
209
|
-
//
|
|
210
|
-
//
|
|
364
|
+
// The flag-detail sub-screen is now self-contained in <FlagDetailEditor>.
|
|
365
|
+
// We just resolve the flag + value pair here so the parent can render
|
|
366
|
+
// the editor and the right-pane preview.
|
|
211
367
|
const detailFlag: AliasFlag | null =
|
|
212
368
|
viewMode.kind === "flag-detail"
|
|
213
369
|
? ALIAS_FLAGS.find((f) => f.id === viewMode.flagId) ?? null
|
|
@@ -215,58 +371,6 @@ export function AliasScreen() {
|
|
|
215
371
|
const detailValue: FlagValue | null =
|
|
216
372
|
detailFlag && config ? config.flags[detailFlag.id] ?? null : null;
|
|
217
373
|
|
|
218
|
-
const detailItems: DetailItem[] = useMemo(() => {
|
|
219
|
-
if (!detailFlag || !detailValue) return [];
|
|
220
|
-
if (detailValue.kind === "text-list") {
|
|
221
|
-
const out: DetailItem[] = [
|
|
222
|
-
{ kind: "toggle", enabled: detailValue.enabled },
|
|
223
|
-
];
|
|
224
|
-
if (detailValue.values.length > 0) {
|
|
225
|
-
out.push({ kind: "header", label: "Values" });
|
|
226
|
-
detailValue.values.forEach((v, i) =>
|
|
227
|
-
out.push({ kind: "value", index: i, text: v }),
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
out.push({ kind: "add" });
|
|
231
|
-
return out;
|
|
232
|
-
}
|
|
233
|
-
if (detailValue.kind === "multi-with-custom") {
|
|
234
|
-
const pickedSet = new Set(detailValue.picked);
|
|
235
|
-
const out: DetailItem[] = [
|
|
236
|
-
{ kind: "toggle", enabled: detailValue.enabled },
|
|
237
|
-
];
|
|
238
|
-
if ((detailFlag.picklist?.length ?? 0) > 0) {
|
|
239
|
-
out.push({ kind: "header", label: "Common filters" });
|
|
240
|
-
for (const tok of detailFlag.picklist!) {
|
|
241
|
-
out.push({ kind: "picklist", token: tok, picked: pickedSet.has(tok) });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (detailValue.custom.length > 0) {
|
|
245
|
-
out.push({ kind: "header", label: "Custom tokens" });
|
|
246
|
-
detailValue.custom.forEach((v, i) =>
|
|
247
|
-
out.push({ kind: "custom", index: i, text: v }),
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
out.push({ kind: "add" });
|
|
251
|
-
return out;
|
|
252
|
-
}
|
|
253
|
-
return [];
|
|
254
|
-
}, [detailFlag, detailValue]);
|
|
255
|
-
|
|
256
|
-
const detailSelectableIndices = useMemo(
|
|
257
|
-
() => detailItems.flatMap((it, i) => (isSelectableDetailItem(it) ? [i] : [])),
|
|
258
|
-
[detailItems],
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
// Reset detail selection on entering / on items shape change.
|
|
262
|
-
useEffect(() => {
|
|
263
|
-
if (viewMode.kind !== "flag-detail") return;
|
|
264
|
-
if (detailSelectableIndices.length === 0) return;
|
|
265
|
-
if (!detailItems[detailSelectedIdx] || !isSelectableDetailItem(detailItems[detailSelectedIdx])) {
|
|
266
|
-
setDetailSelectedIdx(detailSelectableIndices[0]);
|
|
267
|
-
}
|
|
268
|
-
}, [viewMode, detailItems, detailSelectableIndices, detailSelectedIdx]);
|
|
269
|
-
|
|
270
374
|
// ── Helper: check whether a row is gated (requires unmet) ─────────────
|
|
271
375
|
const isGated = useCallback(
|
|
272
376
|
(flag: AliasFlag): boolean => {
|
|
@@ -369,6 +473,16 @@ export function AliasScreen() {
|
|
|
369
473
|
[],
|
|
370
474
|
);
|
|
371
475
|
|
|
476
|
+
// Remove a value from the dev-load flag — invoked when the user deletes a
|
|
477
|
+
// "Dev-loaded channels" row inside the --channels editor. The derived
|
|
478
|
+
// section is sourced from this flag, so the deletion lands here, not in
|
|
479
|
+
// channels.values. updateFlag keeps enabled-state and cascades consistent.
|
|
480
|
+
const removeDevloadChannel = useCallback((val: string) => {
|
|
481
|
+
setConfig((prev) =>
|
|
482
|
+
prev ? { ...prev, flags: withoutDerivedChannel(prev.flags, val) } : prev,
|
|
483
|
+
);
|
|
484
|
+
}, []);
|
|
485
|
+
|
|
372
486
|
const editAliasName = useCallback(async () => {
|
|
373
487
|
if (!config) return;
|
|
374
488
|
const v = await modal.input(
|
|
@@ -400,7 +514,7 @@ export function AliasScreen() {
|
|
|
400
514
|
// so there's no "what does enter do here?" overloading.
|
|
401
515
|
const k = selectedRow.value.kind;
|
|
402
516
|
if (k === "text-list" || k === "multi-with-custom") {
|
|
403
|
-
|
|
517
|
+
// FlagDetailEditor manages its own cursor — parent just flips mode.
|
|
404
518
|
setViewMode({ kind: "flag-detail", flagId: selectedRow.flag.id });
|
|
405
519
|
return;
|
|
406
520
|
}
|
|
@@ -450,11 +564,22 @@ export function AliasScreen() {
|
|
|
450
564
|
}
|
|
451
565
|
|
|
452
566
|
try {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
567
|
+
// If we adopted a lossless hand-written alias, pass its line range so
|
|
568
|
+
// the FIRST write replaces that line in place (absorbing it into the
|
|
569
|
+
// managed block) rather than appending a second alias. Once the block
|
|
570
|
+
// exists, the writer ignores the range and splices by markers.
|
|
571
|
+
const r = await writeAliasToShell(
|
|
572
|
+
config,
|
|
573
|
+
target,
|
|
574
|
+
pendingAdoptRange ? { adoptLineRange: pendingAdoptRange } : {},
|
|
575
|
+
);
|
|
576
|
+
// The adopted line is now gone (replaced by the block); drop the range
|
|
577
|
+
// so subsequent writes don't try to re-target a line that moved.
|
|
578
|
+
if (pendingAdoptRange) setPendingAdoptRange(null);
|
|
579
|
+
// Non-blocking confirmation on the status line — no "press any key".
|
|
580
|
+
const name = r.path.split("/").pop() || r.path;
|
|
581
|
+
setWriteStatus(
|
|
582
|
+
`✓ ${r.action === "created" ? "Created" : "Updated"} ${name} — source it or open a new shell`,
|
|
458
583
|
);
|
|
459
584
|
setShells(await detectShells());
|
|
460
585
|
// We just wrote the rc file with current in-memory flag values, so
|
|
@@ -467,7 +592,7 @@ export function AliasScreen() {
|
|
|
467
592
|
"error",
|
|
468
593
|
);
|
|
469
594
|
}
|
|
470
|
-
}, [config, shells, modal]);
|
|
595
|
+
}, [config, shells, modal, pendingAdoptRange]);
|
|
471
596
|
|
|
472
597
|
// ── Keyboard ──────────────────────────────────────────────────────────
|
|
473
598
|
// j/k navigate the selectable subset, jumping over headers/legend lines.
|
|
@@ -490,154 +615,21 @@ export function AliasScreen() {
|
|
|
490
615
|
[selectableIndices, selectedIdx],
|
|
491
616
|
);
|
|
492
617
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (detailSelectableIndices.length === 0) return;
|
|
497
|
-
const cursorPos = detailSelectableIndices.indexOf(detailSelectedIdx);
|
|
498
|
-
const nextPos =
|
|
499
|
-
cursorPos === -1
|
|
500
|
-
? dir === 1
|
|
501
|
-
? 0
|
|
502
|
-
: detailSelectableIndices.length - 1
|
|
503
|
-
: Math.min(
|
|
504
|
-
detailSelectableIndices.length - 1,
|
|
505
|
-
Math.max(0, cursorPos + dir),
|
|
506
|
-
);
|
|
507
|
-
setDetailSelectedIdx(detailSelectableIndices[nextPos]);
|
|
508
|
-
},
|
|
509
|
-
[detailSelectableIndices, detailSelectedIdx],
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
const handleDetailToggle = useCallback(() => {
|
|
513
|
-
if (!detailFlag || !detailValue) return;
|
|
514
|
-
const item = detailItems[detailSelectedIdx];
|
|
515
|
-
if (!item) return;
|
|
516
|
-
if (item.kind === "toggle") {
|
|
517
|
-
if (detailValue.kind === "text-list") {
|
|
518
|
-
updateFlag(detailFlag.id, { ...detailValue, enabled: !detailValue.enabled });
|
|
519
|
-
} else if (detailValue.kind === "multi-with-custom") {
|
|
520
|
-
updateFlag(detailFlag.id, { ...detailValue, enabled: !detailValue.enabled });
|
|
521
|
-
}
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if (item.kind === "picklist" && detailValue.kind === "multi-with-custom") {
|
|
525
|
-
const pickedSet = new Set(detailValue.picked);
|
|
526
|
-
const next = pickedSet.has(item.token)
|
|
527
|
-
? detailValue.picked.filter((p) => p !== item.token)
|
|
528
|
-
: [...detailValue.picked, item.token];
|
|
529
|
-
updateFlag(detailFlag.id, {
|
|
530
|
-
...detailValue,
|
|
531
|
-
enabled: true,
|
|
532
|
-
picked: next,
|
|
533
|
-
});
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (item.kind === "add") {
|
|
537
|
-
void handleDetailAdd();
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
// value / custom rows: enter does nothing destructive; use `d` to delete.
|
|
541
|
-
}, [detailFlag, detailValue, detailItems, detailSelectedIdx, updateFlag]);
|
|
542
|
-
|
|
543
|
-
const handleDetailAdd = useCallback(async () => {
|
|
544
|
-
if (!detailFlag || !detailValue) return;
|
|
545
|
-
if (detailValue.kind === "text-list") {
|
|
546
|
-
const v = await modal.input(`Add to ${detailFlag.flag}`, "Value");
|
|
547
|
-
if (v === null || v.length === 0) return;
|
|
548
|
-
updateFlag(detailFlag.id, {
|
|
549
|
-
...detailValue,
|
|
550
|
-
enabled: true,
|
|
551
|
-
values: [...detailValue.values, v],
|
|
552
|
-
});
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (detailValue.kind === "multi-with-custom") {
|
|
556
|
-
const v = await modal.input(
|
|
557
|
-
`Add custom token`,
|
|
558
|
-
'Token (e.g. "router" or "!file" for negation)',
|
|
559
|
-
);
|
|
560
|
-
if (v === null || v.length === 0) return;
|
|
561
|
-
updateFlag(detailFlag.id, {
|
|
562
|
-
...detailValue,
|
|
563
|
-
enabled: true,
|
|
564
|
-
custom: [...detailValue.custom, v],
|
|
565
|
-
});
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
}, [detailFlag, detailValue, modal, updateFlag]);
|
|
569
|
-
|
|
570
|
-
const handleDetailDelete = useCallback(async () => {
|
|
571
|
-
if (!detailFlag || !detailValue) return;
|
|
572
|
-
const item = detailItems[detailSelectedIdx];
|
|
573
|
-
if (!item) return;
|
|
574
|
-
if (item.kind === "value" && detailValue.kind === "text-list") {
|
|
575
|
-
const target = detailValue.values[item.index];
|
|
576
|
-
const ok = await modal.confirm(
|
|
577
|
-
"Remove item?",
|
|
578
|
-
`Remove "${truncate(target, 60)}" from ${detailFlag.flag}?`,
|
|
579
|
-
);
|
|
580
|
-
if (!ok) return;
|
|
581
|
-
updateFlag(detailFlag.id, {
|
|
582
|
-
...detailValue,
|
|
583
|
-
values: detailValue.values.filter((_, i) => i !== item.index),
|
|
584
|
-
});
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
if (item.kind === "custom" && detailValue.kind === "multi-with-custom") {
|
|
588
|
-
const target = detailValue.custom[item.index];
|
|
589
|
-
const ok = await modal.confirm(
|
|
590
|
-
"Remove custom token?",
|
|
591
|
-
`Remove "${truncate(target, 60)}" from ${detailFlag.flag}?`,
|
|
592
|
-
);
|
|
593
|
-
if (!ok) return;
|
|
594
|
-
updateFlag(detailFlag.id, {
|
|
595
|
-
...detailValue,
|
|
596
|
-
custom: detailValue.custom.filter((_, i) => i !== item.index),
|
|
597
|
-
});
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
}, [detailFlag, detailValue, detailItems, detailSelectedIdx, modal, updateFlag]);
|
|
601
|
-
|
|
618
|
+
// The flag-detail sub-screen handles its own cursor + key + render
|
|
619
|
+
// (see FlagDetailEditor component). The parent only needs to know how
|
|
620
|
+
// to leave it.
|
|
602
621
|
const exitDetail = useCallback(() => {
|
|
603
622
|
setViewMode({ kind: "main" });
|
|
604
|
-
setDetailSelectedIdx(0);
|
|
605
623
|
}, []);
|
|
606
624
|
|
|
607
625
|
// ── Keyboard handler ──────────────────────────────────────────────────
|
|
608
|
-
//
|
|
626
|
+
// When the sub-screen is mounted, FlagDetailEditor installs its own
|
|
627
|
+
// keyboard handler. Both handlers fire (OpenTUI's useKeyboard delivers
|
|
628
|
+
// to every subscriber), so this handler must SKIP the keys the child
|
|
629
|
+
// owns to avoid double-dispatch. We bail entirely while in detail mode.
|
|
609
630
|
useKeyboardHandler((input, key) => {
|
|
610
631
|
if (state.modal) return;
|
|
611
|
-
|
|
612
|
-
if (viewMode.kind === "flag-detail") {
|
|
613
|
-
if (input === "j" || key.downArrow) {
|
|
614
|
-
moveDetailSelection(1);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (input === "k" || key.upArrow) {
|
|
618
|
-
moveDetailSelection(-1);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
if (key.return || input === " " || key.name === "space") {
|
|
622
|
-
handleDetailToggle();
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
if (input === "a") {
|
|
626
|
-
void handleDetailAdd();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (input === "d") {
|
|
630
|
-
void handleDetailDelete();
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
if (input === "h") {
|
|
634
|
-
exitDetail();
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
// Esc is handled by the global handler — it'll exit the entire screen
|
|
638
|
-
// back to plugins, which is fine. `h` is the documented "back to main".
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
632
|
+
if (viewMode.kind === "flag-detail") return;
|
|
641
633
|
|
|
642
634
|
// Main mode.
|
|
643
635
|
if (input === "j" || key.downArrow) {
|
|
@@ -676,7 +668,8 @@ export function AliasScreen() {
|
|
|
676
668
|
});
|
|
677
669
|
|
|
678
670
|
// ── Render ────────────────────────────────────────────────────────────
|
|
679
|
-
const
|
|
671
|
+
const previewSegments = config ? renderArgs(config) : [];
|
|
672
|
+
const previewAliasName = config?.aliasName ?? "";
|
|
680
673
|
|
|
681
674
|
// Native OpenTUI scrollboxes own the scroll math. We render every item
|
|
682
675
|
// into the scrollbox's content area and ask the renderable to scroll the
|
|
@@ -684,20 +677,22 @@ export function AliasScreen() {
|
|
|
684
677
|
// windowed approach (ScrollableList) overstrike-rendered rows when its
|
|
685
678
|
// height computation drifted off the actual panel size.
|
|
686
679
|
const mainScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
687
|
-
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
688
|
-
|
|
689
|
-
useEffect(() => {
|
|
690
|
-
mainScrollRef.current?.scrollChildIntoView(`alias-row-${selectedIdx}`);
|
|
691
|
-
}, [selectedIdx]);
|
|
692
680
|
|
|
693
681
|
useEffect(() => {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}, [detailSelectedIdx, viewMode]);
|
|
682
|
+
const item = items[selectedIdx];
|
|
683
|
+
if (!item) return;
|
|
684
|
+
mainScrollRef.current?.scrollChildIntoView(`alias-row-${getItemKey(item)}`);
|
|
685
|
+
}, [items, selectedIdx]);
|
|
699
686
|
|
|
700
687
|
const inDetail = viewMode.kind === "flag-detail" && detailFlag && detailValue;
|
|
688
|
+
// FlagDetailEditor only handles text-list and multi-with-custom values;
|
|
689
|
+
// narrow the type for the JSX below.
|
|
690
|
+
const editableDetailValue =
|
|
691
|
+
inDetail &&
|
|
692
|
+
(detailValue.kind === "text-list" ||
|
|
693
|
+
detailValue.kind === "multi-with-custom")
|
|
694
|
+
? detailValue
|
|
695
|
+
: null;
|
|
701
696
|
|
|
702
697
|
const mainListPanel = !config ? (
|
|
703
698
|
<text fg="gray">{error ? `Error: ${error}` : "Loading..."}</text>
|
|
@@ -711,7 +706,9 @@ export function AliasScreen() {
|
|
|
711
706
|
>
|
|
712
707
|
<box flexDirection="column">
|
|
713
708
|
{items.map((item, idx) => (
|
|
714
|
-
|
|
709
|
+
// id is content-derived (not positional `${idx}`) so OpenTUI's
|
|
710
|
+
// reconciler stays in lockstep with React's key-based reconcile.
|
|
711
|
+
<box key={getItemKey(item)} id={`alias-row-${getItemKey(item)}`}>
|
|
715
712
|
{renderItem(item, idx === selectedIdx, isGated)}
|
|
716
713
|
</box>
|
|
717
714
|
))}
|
|
@@ -720,35 +717,66 @@ export function AliasScreen() {
|
|
|
720
717
|
</box>
|
|
721
718
|
);
|
|
722
719
|
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
<
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const listPanel = inDetail ? detailListPanel : mainListPanel;
|
|
720
|
+
const listPanel =
|
|
721
|
+
inDetail && editableDetailValue ? (
|
|
722
|
+
<FlagDetailEditor
|
|
723
|
+
flag={detailFlag}
|
|
724
|
+
value={editableDetailValue}
|
|
725
|
+
onChange={(next) => updateFlag(detailFlag.id, next)}
|
|
726
|
+
onBack={exitDetail}
|
|
727
|
+
derivedValues={
|
|
728
|
+
detailFlag.id === CHANNELS_FLAG_ID && config
|
|
729
|
+
? derivedChannelValues(config.flags)
|
|
730
|
+
: undefined
|
|
731
|
+
}
|
|
732
|
+
derivedLabel="From --dangerously-load:"
|
|
733
|
+
toggleLabel={
|
|
734
|
+
detailFlag.id === CHANNELS_FLAG_ID
|
|
735
|
+
? "Enable own channels"
|
|
736
|
+
: undefined
|
|
737
|
+
}
|
|
738
|
+
onDerivedDelete={(v) => removeDevloadChannel(v)}
|
|
739
|
+
/>
|
|
740
|
+
) : (
|
|
741
|
+
mainListPanel
|
|
742
|
+
);
|
|
748
743
|
|
|
749
|
-
const mainDetailPanel = renderDetail(
|
|
744
|
+
const mainDetailPanel = renderDetail(
|
|
745
|
+
selectedRow,
|
|
746
|
+
isGated,
|
|
747
|
+
previewAliasName,
|
|
748
|
+
previewSegments,
|
|
749
|
+
);
|
|
750
|
+
// When editing --channels and there are derived rows, the right pane carries
|
|
751
|
+
// the contextual instruction about what deleting a derived row does — that
|
|
752
|
+
// explanation belongs here, not crammed into the left list.
|
|
753
|
+
const channelsHasDerived =
|
|
754
|
+
inDetail &&
|
|
755
|
+
detailFlag.id === CHANNELS_FLAG_ID &&
|
|
756
|
+
config &&
|
|
757
|
+
derivedChannelValues(config.flags).length > 0;
|
|
758
|
+
const derivedNote = channelsHasDerived
|
|
759
|
+
? "Rows under “From --dangerously-load:” are derived from the --dangerously-load-development-channels flag. Deleting one here removes it from that flag."
|
|
760
|
+
: undefined;
|
|
761
|
+
// The right-pane `State:` line is informational — it describes what the alias
|
|
762
|
+
// actually runs. For --channels that's the FINAL channel list: the union of
|
|
763
|
+
// the user's own channels and the derived (dev-loaded) ones. We show the
|
|
764
|
+
// union count so it agrees with the list row's count and the Preview, and
|
|
765
|
+
// annotate how many are derived so the source is clear. Computed only when
|
|
766
|
+
// editing --channels; other flags fall back to their own summary.
|
|
767
|
+
const stateOverride =
|
|
768
|
+
inDetail && detailFlag.id === CHANNELS_FLAG_ID && config
|
|
769
|
+
? channelsStateSummary(config.flags)
|
|
770
|
+
: undefined;
|
|
750
771
|
const detailDetailPanel = inDetail
|
|
751
|
-
? renderFlagDetailRightPane(
|
|
772
|
+
? renderFlagDetailRightPane(
|
|
773
|
+
detailFlag,
|
|
774
|
+
detailValue,
|
|
775
|
+
previewAliasName,
|
|
776
|
+
previewSegments,
|
|
777
|
+
derivedNote,
|
|
778
|
+
stateOverride,
|
|
779
|
+
)
|
|
752
780
|
: null;
|
|
753
781
|
const detailPanel = inDetail ? detailDetailPanel : mainDetailPanel;
|
|
754
782
|
|
|
@@ -757,9 +785,11 @@ export function AliasScreen() {
|
|
|
757
785
|
: 0;
|
|
758
786
|
const subtitle = inDetail
|
|
759
787
|
? `editing ${detailFlag.flag}`
|
|
760
|
-
:
|
|
761
|
-
?
|
|
762
|
-
:
|
|
788
|
+
: writeStatus
|
|
789
|
+
? writeStatus
|
|
790
|
+
: config
|
|
791
|
+
? `${enabledCount} flag${enabledCount === 1 ? "" : "s"} enabled`
|
|
792
|
+
: "";
|
|
763
793
|
|
|
764
794
|
// Resolve the rc-file name we'd actually write to (basename of the chosen
|
|
765
795
|
// shell's path). Falls back to "shell" if no shells were detected — UI
|
|
@@ -778,15 +808,15 @@ export function AliasScreen() {
|
|
|
778
808
|
? rcSnapshot !== null && !shallowFlagsEqual(config.flags, rcSnapshot)
|
|
779
809
|
: false;
|
|
780
810
|
|
|
781
|
-
const footerHints = inDetail
|
|
782
|
-
?
|
|
811
|
+
const footerHints: FooterHint[] = inDetail
|
|
812
|
+
? [
|
|
783
813
|
{ keys: ["↑", "↓"], label: "move" },
|
|
784
814
|
{ keys: ["space"], label: "toggle" },
|
|
785
815
|
{ keys: ["a"], label: "add" },
|
|
786
816
|
{ keys: ["d"], label: "delete" },
|
|
787
817
|
{ keys: ["h"], label: "back" },
|
|
788
|
-
]
|
|
789
|
-
:
|
|
818
|
+
]
|
|
819
|
+
: [
|
|
790
820
|
{ keys: ["↑", "↓"], label: "move" },
|
|
791
821
|
{ keys: ["space"], label: "toggle" },
|
|
792
822
|
{ keys: ["e"], label: "edit" },
|
|
@@ -795,7 +825,7 @@ export function AliasScreen() {
|
|
|
795
825
|
keys: ["w"],
|
|
796
826
|
label: `${dirty ? "*" : ""}write to ${writeLabel}`,
|
|
797
827
|
},
|
|
798
|
-
]
|
|
828
|
+
];
|
|
799
829
|
|
|
800
830
|
return (
|
|
801
831
|
<ScreenLayout
|
|
@@ -811,6 +841,38 @@ export function AliasScreen() {
|
|
|
811
841
|
|
|
812
842
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
813
843
|
|
|
844
|
+
/**
|
|
845
|
+
* Summarize the EFFECTIVE state of `--channels` for the detail `State:` line:
|
|
846
|
+
* the final channel list that the alias will actually run, i.e. the union of
|
|
847
|
+
* the user's own (enabled) channels and the derived dev-loaded ones. This must
|
|
848
|
+
* agree with the list row's union count and the Preview.
|
|
849
|
+
*
|
|
850
|
+
* Returns "off" when nothing is emitted, or e.g.:
|
|
851
|
+
* - "2 channels" (all own, no derived)
|
|
852
|
+
* - "1 channel (from --dangerously-load)" (purely derived, own off/empty)
|
|
853
|
+
* - "2 channels (1 from --dangerously-load)" (mixed)
|
|
854
|
+
*/
|
|
855
|
+
function channelsStateSummary(flags: Record<string, FlagValue>): string {
|
|
856
|
+
const ownValue = flags[CHANNELS_FLAG_ID];
|
|
857
|
+
const ownValues =
|
|
858
|
+
ownValue && ownValue.kind === "text-list" && ownValue.enabled
|
|
859
|
+
? ownValue.values.filter((v) => v.length > 0)
|
|
860
|
+
: [];
|
|
861
|
+
const derived = derivedChannelValues(flags);
|
|
862
|
+
// Union, deduped, matching the writer's render-time union.
|
|
863
|
+
const total = new Set([...ownValues, ...derived]).size;
|
|
864
|
+
if (total === 0) return "off";
|
|
865
|
+
const noun = total === 1 ? "channel" : "channels";
|
|
866
|
+
if (derived.length === 0) return `${total} ${noun}`;
|
|
867
|
+
// How many of the union come from the derived flag (after dedup with own).
|
|
868
|
+
const derivedInUnion = new Set(derived).size;
|
|
869
|
+
const derivedNote =
|
|
870
|
+
derivedInUnion === total
|
|
871
|
+
? "from --dangerously-load"
|
|
872
|
+
: `${derivedInUnion} from --dangerously-load`;
|
|
873
|
+
return `${total} ${noun} (${derivedNote})`;
|
|
874
|
+
}
|
|
875
|
+
|
|
814
876
|
/**
|
|
815
877
|
* Compare two flag maps for value equality. We render-then-diff via JSON
|
|
816
878
|
* because FlagValues are small, fully serialisable, and order-insensitive
|
|
@@ -849,6 +911,47 @@ function isFlagEnabled(value: FlagValue | undefined): boolean {
|
|
|
849
911
|
}
|
|
850
912
|
}
|
|
851
913
|
|
|
914
|
+
/**
|
|
915
|
+
* Pick a managed-alias name that can't shadow a surviving hand-written alias.
|
|
916
|
+
*
|
|
917
|
+
* On a not-lossless adoption we keep the user's original `<original>` line in
|
|
918
|
+
* the rc file, so the managed block must NOT share its name (shell aliases are
|
|
919
|
+
* last-definition-wins — a same-named block would override the original and
|
|
920
|
+
* silently drop the very tokens that made it not-lossless).
|
|
921
|
+
*
|
|
922
|
+
* Strategy: prefer the tool's `current` name if it already differs from the
|
|
923
|
+
* original. Otherwise derive `<original>m`, `<original>m2`, … until we find one
|
|
924
|
+
* that is valid AND collides with neither the original nor the other detected
|
|
925
|
+
* claude aliases. Falls back to a timestamp-free deterministic suffix.
|
|
926
|
+
*/
|
|
927
|
+
function pickDistinctName(
|
|
928
|
+
current: string,
|
|
929
|
+
original: string,
|
|
930
|
+
others: string[],
|
|
931
|
+
): string {
|
|
932
|
+
// Names already taken by aliases we're leaving in place.
|
|
933
|
+
const taken = new Set<string>([original]);
|
|
934
|
+
for (const line of others) {
|
|
935
|
+
const m = /^alias\s+([A-Za-z_][A-Za-z0-9_-]*)\s*=/.exec(line.trim());
|
|
936
|
+
if (m) taken.add(m[1]);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// If our current name is already distinct + valid, just use it.
|
|
940
|
+
if (current !== original && !taken.has(current) && !validateAliasName(current)) {
|
|
941
|
+
return current;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Derive a suffixed variant from the original.
|
|
945
|
+
const base = `${original}m`;
|
|
946
|
+
if (!taken.has(base) && !validateAliasName(base)) return base;
|
|
947
|
+
for (let n = 2; n < 100; n++) {
|
|
948
|
+
const cand = `${original}m${n}`;
|
|
949
|
+
if (!taken.has(cand) && !validateAliasName(cand)) return cand;
|
|
950
|
+
}
|
|
951
|
+
// Pathological fallback — should never be reached for sane inputs.
|
|
952
|
+
return `${original}managed`;
|
|
953
|
+
}
|
|
954
|
+
|
|
852
955
|
/**
|
|
853
956
|
* Produce an "enabled by default" value for a flag — what we'd give it when
|
|
854
957
|
* the user activates a dependent that needs this one on. For booleans this
|
|
@@ -1149,13 +1252,16 @@ function renderRow(
|
|
|
1149
1252
|
selected: boolean,
|
|
1150
1253
|
gated: boolean,
|
|
1151
1254
|
): React.ReactNode {
|
|
1152
|
-
|
|
1255
|
+
// Prefer the precomputed effective state (e.g. --channels shows enabled when
|
|
1256
|
+
// a dev-loaded channel forces it to emit) over the flag's own enabled flag.
|
|
1257
|
+
const enabled = row.effectiveEnabled ?? isFlagEnabled(row.value);
|
|
1153
1258
|
const marker = enabled ? "[x]" : "[ ]";
|
|
1154
1259
|
const issueMark = row.issue ? " ⚠" : "";
|
|
1155
1260
|
const fg = gated ? "gray" : enabled ? "white" : "gray";
|
|
1156
1261
|
// Only show a value suffix when the flag carries non-redundant detail.
|
|
1157
|
-
// Boolean / tri-state state is fully conveyed by the checkbox.
|
|
1158
|
-
|
|
1262
|
+
// Boolean / tri-state state is fully conveyed by the checkbox. A precomputed
|
|
1263
|
+
// effectiveDetail (e.g. --channels union count) wins over the own-value one.
|
|
1264
|
+
const detail = row.effectiveDetail ?? enabledDetail(row.value);
|
|
1159
1265
|
const detailSuffix = detail ? ` ${detail}` : "";
|
|
1160
1266
|
const line = `${selected ? "▶ " : " "}${marker} ${row.flag.flag}${detailSuffix}${issueMark}`;
|
|
1161
1267
|
if (selected) {
|
|
@@ -1204,7 +1310,8 @@ function enabledDetail(value: FlagValue): string {
|
|
|
1204
1310
|
function renderDetail(
|
|
1205
1311
|
row: Row | undefined,
|
|
1206
1312
|
isGated: (f: AliasFlag) => boolean,
|
|
1207
|
-
|
|
1313
|
+
aliasName: string,
|
|
1314
|
+
segments: Segment[],
|
|
1208
1315
|
): React.ReactNode {
|
|
1209
1316
|
if (!row) {
|
|
1210
1317
|
return <text fg="gray">No row selected.</text>;
|
|
@@ -1217,13 +1324,15 @@ function renderDetail(
|
|
|
1217
1324
|
|
|
1218
1325
|
// Preview is the only "global" element kept in the detail panel — it
|
|
1219
1326
|
// updates as the user toggles, which is the strongest visual feedback for
|
|
1220
|
-
// "what alias am I building".
|
|
1327
|
+
// "what alias am I building". We show argv one-per-line rather than the
|
|
1328
|
+
// raw POSIX-escaped alias body (which contains `'\''` sequences that look
|
|
1329
|
+
// alarming to readers who don't know the close-escape-reopen pattern).
|
|
1221
1330
|
return (
|
|
1222
1331
|
<box flexDirection="column">
|
|
1223
1332
|
{headerAndBody}
|
|
1224
1333
|
<text> </text>
|
|
1225
|
-
<text fg="gray">Preview
|
|
1226
|
-
|
|
1334
|
+
<text fg="gray">Preview:</text>
|
|
1335
|
+
{renderPreviewLines(aliasName, segments)}
|
|
1227
1336
|
</box>
|
|
1228
1337
|
);
|
|
1229
1338
|
}
|
|
@@ -1256,6 +1365,10 @@ function renderFlagDetail(
|
|
|
1256
1365
|
isGated: (f: AliasFlag) => boolean,
|
|
1257
1366
|
): React.ReactNode {
|
|
1258
1367
|
const gated = isGated(row.flag);
|
|
1368
|
+
// State must agree with the checkbox and the Preview. For --channels we show
|
|
1369
|
+
// the effective channel-list summary (union of own + derived); other flags
|
|
1370
|
+
// use their own value summary.
|
|
1371
|
+
const stateText = row.stateSummary ?? summarizeValue(row.value);
|
|
1259
1372
|
return (
|
|
1260
1373
|
<box flexDirection="column">
|
|
1261
1374
|
<text fg="white">
|
|
@@ -1265,7 +1378,7 @@ function renderFlagDetail(
|
|
|
1265
1378
|
<text> </text>
|
|
1266
1379
|
<text>
|
|
1267
1380
|
<span fg="gray">State: </span>
|
|
1268
|
-
<span fg="white">{
|
|
1381
|
+
<span fg="white">{stateText}</span>
|
|
1269
1382
|
</text>
|
|
1270
1383
|
{gated && (
|
|
1271
1384
|
<text fg="gray">
|
|
@@ -1279,103 +1392,116 @@ function renderFlagDetail(
|
|
|
1279
1392
|
);
|
|
1280
1393
|
}
|
|
1281
1394
|
|
|
1282
|
-
function firstLine(block: string, skip: number): string {
|
|
1283
|
-
const lines = block.split("\n");
|
|
1284
|
-
return lines[skip] ?? "";
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
1395
|
/**
|
|
1288
|
-
* Render
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1396
|
+
* Render the alias preview as one argv arg per line. This replaces the raw
|
|
1397
|
+
* POSIX-escaped alias body (`alias c='claude '\''text with spaces'\'' …'`),
|
|
1398
|
+
* which is correct but visually alarming — readers without POSIX shell
|
|
1399
|
+
* fluency mistake `'\''` for broken quoting.
|
|
1400
|
+
*
|
|
1401
|
+
* We show each argv token on its own row, with values that need quoting
|
|
1402
|
+
* wrapped in plain double quotes (display-only — these are NOT what gets
|
|
1403
|
+
* written to the rc file). Composite segments (templated values) display
|
|
1404
|
+
* with their `{token}` placeholders intact in cyan so users see exactly
|
|
1405
|
+
* what will expand at shell-eval time.
|
|
1406
|
+
*
|
|
1407
|
+
* Example output:
|
|
1408
|
+
* alias c='claude
|
|
1409
|
+
* --dangerously-skip-permissions
|
|
1410
|
+
* --append-system-prompt "you ae the smaretes model ever"
|
|
1411
|
+
* --debug plugins
|
|
1412
|
+
* '
|
|
1292
1413
|
*/
|
|
1293
|
-
function
|
|
1294
|
-
|
|
1414
|
+
function renderPreviewLines(
|
|
1415
|
+
aliasName: string,
|
|
1416
|
+
segments: Segment[],
|
|
1295
1417
|
): React.ReactNode {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1418
|
+
if (segments.length === 0) {
|
|
1419
|
+
return <text fg="white">{`alias ${aliasName || "?"}='claude'`}</text>;
|
|
1420
|
+
}
|
|
1421
|
+
const lines: React.ReactNode[] = [
|
|
1422
|
+
<text key="head" fg="white">{`alias ${aliasName || "?"}='claude`}</text>,
|
|
1423
|
+
];
|
|
1424
|
+
segments.forEach((seg, i) => {
|
|
1425
|
+
lines.push(
|
|
1426
|
+
<text key={`arg-${i}`} fg="white">
|
|
1427
|
+
{" "}
|
|
1428
|
+
{renderPreviewSegment(seg)}
|
|
1429
|
+
</text>,
|
|
1430
|
+
);
|
|
1431
|
+
});
|
|
1432
|
+
lines.push(
|
|
1433
|
+
<text key="tail" fg="white">
|
|
1434
|
+
{"'"}
|
|
1435
|
+
</text>,
|
|
1436
|
+
);
|
|
1437
|
+
return <>{lines}</>;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function renderPreviewSegment(seg: Segment): React.ReactNode {
|
|
1441
|
+
if (seg.kind === "literal") {
|
|
1442
|
+
return <span fg="white">{quoteForDisplay(seg.text)}</span>;
|
|
1443
|
+
}
|
|
1444
|
+
// Composite: literal parts as-is, raw parts as `{token}` reconstructed
|
|
1445
|
+
// from the writer's TOKEN_TO_SUB map. We don't have direct access to the
|
|
1446
|
+
// map here, so we reverse-lookup using the same forms.
|
|
1447
|
+
const spans: React.ReactNode[] = [];
|
|
1448
|
+
seg.parts.forEach((p, i) => {
|
|
1449
|
+
if (p.kind === "literal") {
|
|
1450
|
+
spans.push(
|
|
1451
|
+
<span key={`p-${i}`} fg="white">
|
|
1452
|
+
{p.text}
|
|
1453
|
+
</span>,
|
|
1454
|
+
);
|
|
1455
|
+
} else {
|
|
1456
|
+
spans.push(
|
|
1457
|
+
<span key={`p-${i}`} fg="cyan">
|
|
1458
|
+
{substitutionToToken(p.posix)}
|
|
1302
1459
|
</span>,
|
|
1303
1460
|
);
|
|
1304
1461
|
}
|
|
1305
|
-
nodes.push(
|
|
1306
|
-
<span key={`k-${i}`} bg="#3f3f46" fg="#fafafa">
|
|
1307
|
-
{` ${hint.keys.join("")} `}
|
|
1308
|
-
</span>,
|
|
1309
|
-
);
|
|
1310
|
-
nodes.push(
|
|
1311
|
-
<span key={`l-${i}`} fg="gray">
|
|
1312
|
-
{` ${hint.label}`}
|
|
1313
|
-
</span>,
|
|
1314
|
-
);
|
|
1315
1462
|
});
|
|
1316
|
-
return <
|
|
1463
|
+
return <span>{spans}</span>;
|
|
1317
1464
|
}
|
|
1318
1465
|
|
|
1319
|
-
|
|
1466
|
+
const PREVIEW_SAFE = /^[A-Za-z0-9_\-=,:.\/!@%+]+$/;
|
|
1320
1467
|
|
|
1321
|
-
function
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
return `pick:${item.token}`;
|
|
1329
|
-
case "custom":
|
|
1330
|
-
return `custom:${item.index}`;
|
|
1331
|
-
case "add":
|
|
1332
|
-
return "add";
|
|
1333
|
-
case "header":
|
|
1334
|
-
return `header:${item.label}:${idx}`;
|
|
1335
|
-
}
|
|
1468
|
+
function quoteForDisplay(text: string): string {
|
|
1469
|
+
if (text === "") return '""';
|
|
1470
|
+
if (PREVIEW_SAFE.test(text)) return text;
|
|
1471
|
+
// Display-only quoting. We don't have to escape anything for the shell
|
|
1472
|
+
// since this string never reaches a parser — it's just for the user to
|
|
1473
|
+
// read. We do escape an embedded `"` to keep visual balance.
|
|
1474
|
+
return `"${text.replace(/"/g, '\\"')}"`;
|
|
1336
1475
|
}
|
|
1337
1476
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
case "
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
case "picklist":
|
|
1355
|
-
body = `[${item.picked ? "x" : " "}] ${item.token}`;
|
|
1356
|
-
break;
|
|
1357
|
-
case "custom":
|
|
1358
|
-
body = item.text;
|
|
1359
|
-
break;
|
|
1360
|
-
case "add":
|
|
1361
|
-
body = "+ Add new...";
|
|
1362
|
-
break;
|
|
1363
|
-
}
|
|
1364
|
-
const line = `${cursor}${body}`;
|
|
1365
|
-
if (isSelected) {
|
|
1366
|
-
return (
|
|
1367
|
-
<text bg="#7e57c2" fg="white">
|
|
1368
|
-
{line}
|
|
1369
|
-
</text>
|
|
1370
|
-
);
|
|
1477
|
+
/**
|
|
1478
|
+
* Reverse-lookup the writer's POSIX substitution codes to their `{token}`
|
|
1479
|
+
* forms. Mirror of `TOKEN_TO_SUB` in `alias-shell-writer.ts`.
|
|
1480
|
+
*/
|
|
1481
|
+
function substitutionToToken(posixCode: string): string {
|
|
1482
|
+
switch (posixCode) {
|
|
1483
|
+
case '$(basename "$PWD")':
|
|
1484
|
+
return "{folder}";
|
|
1485
|
+
case "$(date +%d)":
|
|
1486
|
+
return "{day}";
|
|
1487
|
+
case "$(date +%m)":
|
|
1488
|
+
return "{month}";
|
|
1489
|
+
case "$(date +%Y)":
|
|
1490
|
+
return "{year}";
|
|
1491
|
+
default:
|
|
1492
|
+
return posixCode;
|
|
1371
1493
|
}
|
|
1372
|
-
return <text fg="white">{line}</text>;
|
|
1373
1494
|
}
|
|
1374
1495
|
|
|
1496
|
+
|
|
1497
|
+
|
|
1375
1498
|
function renderFlagDetailRightPane(
|
|
1376
1499
|
flag: AliasFlag,
|
|
1377
1500
|
value: FlagValue,
|
|
1378
|
-
|
|
1501
|
+
aliasName: string,
|
|
1502
|
+
segments: Segment[],
|
|
1503
|
+
derivedNote?: string,
|
|
1504
|
+
stateOverride?: string,
|
|
1379
1505
|
): React.ReactNode {
|
|
1380
1506
|
return (
|
|
1381
1507
|
<box flexDirection="column">
|
|
@@ -1383,14 +1509,20 @@ function renderFlagDetailRightPane(
|
|
|
1383
1509
|
<strong>{flag.flag}</strong>
|
|
1384
1510
|
</text>
|
|
1385
1511
|
<text fg="gray">{flag.description}</text>
|
|
1512
|
+
{derivedNote && (
|
|
1513
|
+
<>
|
|
1514
|
+
<text> </text>
|
|
1515
|
+
<text fg="gray">{derivedNote}</text>
|
|
1516
|
+
</>
|
|
1517
|
+
)}
|
|
1386
1518
|
<text> </text>
|
|
1387
1519
|
<text>
|
|
1388
1520
|
<span fg="gray">State: </span>
|
|
1389
|
-
<span fg="white">{summarizeValue(value)}</span>
|
|
1521
|
+
<span fg="white">{stateOverride ?? summarizeValue(value)}</span>
|
|
1390
1522
|
</text>
|
|
1391
1523
|
<text> </text>
|
|
1392
|
-
<text fg="gray">Preview
|
|
1393
|
-
|
|
1524
|
+
<text fg="gray">Preview:</text>
|
|
1525
|
+
{renderPreviewLines(aliasName, segments)}
|
|
1394
1526
|
<text> </text>
|
|
1395
1527
|
<text fg="gray">Keys:</text>
|
|
1396
1528
|
<text fg="white"> space/enter toggle the highlighted row</text>
|