sprag-cli 3.43.0 → 3.45.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/cli.js CHANGED
@@ -209,7 +209,7 @@ async function main() {
209
209
  // sprag route-scan --hook # SessionStart hook mode (context injection)
210
210
  // sprag route-scan dismiss <N> # mute candidate R<N>
211
211
  // Promote a candidate to a ratchet rule (scope is always explicit):
212
- // sprag harness promote R<N> --project|--global
212
+ // sprag harness promote R<N> --global|--project
213
213
  // brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
214
214
  // briefing of state the statusline can only chip (ctx tier crossings,
215
215
  // mid-session route/rule-health changes). Silent when nothing changed.
@@ -397,9 +397,10 @@ async function main() {
397
397
  process.exit(1);
398
398
  }
399
399
  const { formatReport } = await import('../src/formatters/statusline.js');
400
- const isIcon = hasFlag('--icon')
401
- ? true
402
- : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
400
+ // Same resolver as the real statusline path: a demo that renders emoji
401
+ // where the real line would not is worse than no demo at all.
402
+ const { resolveLabelMode } = await import('../src/statusline-mode.js');
403
+ const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
403
404
  const isVerbose = hasFlag('--verbose')
404
405
  ? true
405
406
  : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
@@ -413,7 +414,7 @@ async function main() {
413
414
  color: colorOk,
414
415
  verbose: isVerbose,
415
416
  timer: showTimer,
416
- mode: isIcon ? 'icon' : 'text',
417
+ mode: labelMode,
417
418
  segments,
418
419
  singleLine: hasFlag('--single-line'),
419
420
  });
@@ -480,9 +481,11 @@ async function main() {
480
481
  const { statuslineDefaults } = await import('../src/config.js');
481
482
  const cfg = statuslineDefaults();
482
483
  const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
483
- const isIcon = hasFlag('--icon')
484
- ? true
485
- : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
484
+ // Third caller of the same decision — the no-session line has to agree
485
+ // with the populated one, or IntelliJ users watch icons appear and
486
+ // disappear as the analysis window empties and fills.
487
+ const { resolveLabelMode } = await import('../src/statusline-mode.js');
488
+ const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
486
489
  const stdinJson = readStdinJson();
487
490
  const caps = extractCaps(stdinJson);
488
491
  const model = extractModel(stdinJson);
@@ -494,7 +497,7 @@ async function main() {
494
497
  }
495
498
  console.log(formatNoSession(
496
499
  { caps, model, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
497
- { color: colorOk, mode: isIcon ? 'icon' : 'text' },
500
+ { color: colorOk, mode: labelMode },
498
501
  ));
499
502
  return;
500
503
  }
@@ -749,21 +752,11 @@ async function main() {
749
752
  const { statuslineDefaults } = await import('../src/config.js');
750
753
  const cfg = statuslineDefaults();
751
754
 
752
- // IntelliJ's Claude Code plugin renders the statusline through a custom
753
- // widget that fuses prior frames with the new one when emoji are present,
754
- // producing garbage like "59:548" that no ANSI escape can clean up
755
- // (verified: emitting the same output directly into JediTerm renders
756
- // cleanly, so the bug is in the plugin's render path, not the terminal).
757
- // Force text mode unconditionally inside IntelliJ — even past an explicit
758
- // `--icon` flag, since wrappers commonly hardcode `--icon` and the user
759
- // can't easily edit them; icon mode is just broken there.
760
- const isIntelliJ = process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
761
- // CLI flags take precedence; otherwise fall back to persisted config.
762
- const isIcon = isIntelliJ
763
- ? false
764
- : (hasFlag('--icon')
765
- ? true
766
- : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon));
755
+ // Icon vs text is decided in one module shared with the `--demo` path, so
756
+ // the two cannot drift apart again. It also owns the IntelliJ downgrade and
757
+ // the escape hatch out of it (SPRAG_ICON=force / `sprag mode icon-force`).
758
+ const { resolveLabelMode } = await import('../src/statusline-mode.js');
759
+ const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
767
760
  const isVerbose = hasFlag('--verbose')
768
761
  ? true
769
762
  : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
@@ -779,7 +772,7 @@ async function main() {
779
772
  color: colorOk,
780
773
  verbose: isVerbose,
781
774
  timer: showTimer,
782
- mode: isIcon ? 'icon' : 'text',
775
+ mode: labelMode,
783
776
  segments,
784
777
  // macOS builds of Claude Code have rendered only the first line of a
785
778
  // multi-line statusline in some versions (anthropics/claude-code#35176)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.43.0",
3
+ "version": "3.45.0",
4
4
  "description": "Sprag - a quality ratchet harness for AI coding agents: model-fitting delegation, ratchet rules, doc2md, Korean style gate. Same tool as claude-token-saver, under its new name.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,10 @@
21
21
  "agent": "haiku-runner",
22
22
  "example": "npm test 돌려봐",
23
23
  "exampleEn": "run npm test",
24
- "budget": { "calls": 8, "out": 1500 }
24
+ "budget": {
25
+ "calls": 8,
26
+ "out": 1500
27
+ }
25
28
  },
26
29
  {
27
30
  "id": "run-t1",
@@ -32,7 +35,10 @@
32
35
  "agent": "sonnet",
33
36
  "example": "빌드하고 실패한 테스트만 보여줘",
34
37
  "exampleEn": "build it and show only the failing tests",
35
- "budget": { "calls": null, "out": 6000 }
38
+ "budget": {
39
+ "calls": null,
40
+ "out": 6000
41
+ }
36
42
  },
37
43
  {
38
44
  "id": "explore-t2",
@@ -43,7 +49,10 @@
43
49
  "agent": "haiku-explore",
44
50
  "example": "이 설정값 어느 파일에 있어?",
45
51
  "exampleEn": "which file holds this config value?",
46
- "budget": { "calls": 8, "out": 1500 }
52
+ "budget": {
53
+ "calls": 8,
54
+ "out": 1500
55
+ }
47
56
  },
48
57
  {
49
58
  "id": "explore-t1",
@@ -54,7 +63,10 @@
54
63
  "agent": "sonnet",
55
64
  "example": "이 함수를 호출하는 곳 전부 찾아줘",
56
65
  "exampleEn": "find every caller of this function",
57
- "budget": { "calls": null, "out": 6000 }
66
+ "budget": {
67
+ "calls": null,
68
+ "out": 6000
69
+ }
58
70
  },
59
71
  {
60
72
  "id": "check-t2",
@@ -65,7 +77,10 @@
65
77
  "agent": "haiku-explore",
66
78
  "example": "지금 배포된 버전이 뭔지 확인해줘",
67
79
  "exampleEn": "check which version is deployed right now",
68
- "budget": { "calls": 8, "out": 1500 }
80
+ "budget": {
81
+ "calls": 8,
82
+ "out": 1500
83
+ }
69
84
  },
70
85
  {
71
86
  "id": "check-t1",
@@ -76,7 +91,10 @@
76
91
  "agent": "sonnet",
77
92
  "example": "마이그레이션이 실제로 적용됐는지 확인해줘",
78
93
  "exampleEn": "verify the migration actually applied",
79
- "budget": { "calls": null, "out": 6000 }
94
+ "budget": {
95
+ "calls": null,
96
+ "out": 6000
97
+ }
80
98
  },
81
99
  {
82
100
  "id": "paste-t2",
@@ -87,7 +105,10 @@
87
105
  "agent": "haiku-explore",
88
106
  "example": "이 에러 로그는 무슨 뜻이야?",
89
107
  "exampleEn": "what does this error log mean?",
90
- "budget": { "calls": 8, "out": 1500 }
108
+ "budget": {
109
+ "calls": 8,
110
+ "out": 1500
111
+ }
91
112
  },
92
113
  {
93
114
  "id": "paste-t1",
@@ -98,7 +119,10 @@
98
119
  "agent": "sonnet",
99
120
  "example": "이 스택트레이스에서 어느 모듈이 죽은 건지 짚어줘",
100
121
  "exampleEn": "point out which module failed in this stack trace",
101
- "budget": { "calls": null, "out": 8000 }
122
+ "budget": {
123
+ "calls": null,
124
+ "out": 8000
125
+ }
102
126
  },
103
127
  {
104
128
  "id": "read-t2",
@@ -109,7 +133,24 @@
109
133
  "agent": "haiku-explore",
110
134
  "example": "이 README 요약해줘",
111
135
  "exampleEn": "summarize this README",
112
- "budget": { "calls": 8, "out": 1900 }
136
+ "budget": {
137
+ "calls": 8,
138
+ "out": 1900
139
+ }
140
+ },
141
+ {
142
+ "id": "read-t1",
143
+ "tier": "T1",
144
+ "category": "read",
145
+ "label": "읽기·요약·설명",
146
+ "labelEn": "reading / summarizing / explaining",
147
+ "agent": "sonnet",
148
+ "example": "이 함수가 하는 일을 단계별로 정리해줘",
149
+ "exampleEn": "walk through what this function does, step by step",
150
+ "budget": {
151
+ "calls": null,
152
+ "out": 8000
153
+ }
113
154
  }
114
155
  ]
115
156
  }
@@ -15,8 +15,8 @@
15
15
  "en": "When a web fetch/search is blocked by robots or user-agent checks ('unable to fetch' and similar) → retry with curl carrying a browser UA header (Mozilla/5.0 ... Chrome/...). Sites like reddit also answer on their .json endpoints with that UA"
16
16
  },
17
17
  {
18
- "ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(project/global)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --project/--global 플래그를 명시해 실행",
19
- "en": "Ask the user for the scope (project or global) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --project/--global explicitly"
18
+ "ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(global/project)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --global/--project 플래그를 명시해 실행",
19
+ "en": "Ask the user for the scope (global or project) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --global/--project explicitly"
20
20
  },
21
21
  {
22
22
  "ko": "자동화 도구가 grep·정규식으로 찾는 마커 텍스트(헤더·앵커·키워드 라인)는 리팩터링·압축·수정 금지 — 정확 매칭 검출은 한 글자만 바뀌어도 깨짐",
package/src/brief.js CHANGED
@@ -226,7 +226,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
226
226
  }
227
227
  for (const [kind, x] of fresh) {
228
228
  items.push(kind === 'route'
229
- ? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --project|--global (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
229
+ ? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --global|--project (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
230
230
  : `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (${x.healthSource === 'delegated' ? `위임 실행 ${x.delegatedRuns}건 중 에러율 ${Math.round((x.delegatedErrRate || 0) * 100)}%` : `유사 에피소드 기준 에러율 ${Math.round((x.errRate || 0) * 100)}%`}, statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: sprag route-scan rules`);
231
231
  }
232
232
  s.briefed = [...briefed];
@@ -7,7 +7,7 @@
7
7
  * sprag route-scan --hook # SessionStart hook mode (context injection)
8
8
  * sprag route-scan dismiss <N> # mute candidate R<N>
9
9
  * Promote a candidate to a ratchet rule (scope is always explicit):
10
- * sprag harness promote R<N> --project|--global
10
+ * sprag harness promote R<N> --global|--project
11
11
  * brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
12
12
  * briefing of state the statusline can only chip (ctx tier crossings,
13
13
  * mid-session route/rule-health changes). Silent when nothing changed.
@@ -22,10 +22,22 @@ export async function run({ hasFlag }) {
22
22
  process.exit(1);
23
23
  }
24
24
  const ctx = readStdinJson() || {};
25
+ const parts = [];
25
26
  try {
26
27
  const { runBrief } = await import('../brief.js');
27
28
  const out = await runBrief({ sessionId: ctx.session_id, transcriptPath: ctx.transcript_path });
28
- if (out) console.log(out);
29
+ if (out) parts.push(out);
29
30
  } catch (e) { debug('brief:hook', e); /* briefing is best-effort — never block a prompt */ }
31
+ // State the matching delegation rule as a fact about this request. Registered
32
+ // rules were going unused because applying one meant scanning a list and
33
+ // resolving it against a general "don't spawn subagents" instruction; naming
34
+ // the match here removes both steps. Silent unless it is sure — see
35
+ // src/route-inject.js.
36
+ try {
37
+ const { routeHint } = await import('../route-inject.js');
38
+ const hint = routeHint(ctx.prompt);
39
+ if (hint) parts.push(hint);
40
+ } catch (e) { debug('brief:route-hint', e); }
41
+ if (parts.length) console.log(parts.join('\n'));
30
42
  return;
31
43
  }
@@ -10,12 +10,28 @@
10
10
  export async function run({ args }) {
11
11
  const { applyMode, loadConfig, configPath, statuslineDefaults, userLanguage, VALID_KEYWORDS } =
12
12
  await import('../config.js');
13
+ const { resolveLabelMode, isJetBrainsTerminal } = await import('../statusline-mode.js');
13
14
  const words = args.slice(1);
14
15
  if (words.length === 0) {
15
16
  const eff = statuslineDefaults();
16
17
  const raw = loadConfig();
17
18
  console.log('Statusline (effective):');
18
- console.log(` icon: ${eff.icon}`);
19
+ console.log(` labels: ${eff.labels}`);
20
+ // What renders can differ from the stored preference: inside IntelliJ the
21
+ // emoji set steps down to narrow glyphs, because the IDE font lacks the
22
+ // emoji and the fallback breaks the cell grid. Saying so here is the
23
+ // difference between a two-second answer and a hunt through the source for
24
+ // why `--icon` appeared to do nothing.
25
+ const { mode: labelMode, reason } = resolveLabelMode({ cfg: eff });
26
+ if (labelMode !== eff.labels) {
27
+ const why = reason === 'intellij-narrow'
28
+ ? ' (IntelliJ: emoji garble in the IDE font. `sprag mode icon-force` keeps them anyway)'
29
+ : reason === 'intellij-forced'
30
+ ? ' (icon-force)'
31
+ : '';
32
+ console.log(` renders: ${labelMode}${why}`);
33
+ }
34
+ console.log(` icon-force: ${eff.iconForce}`);
19
35
  console.log(` verbose: ${eff.verbose}`);
20
36
  console.log(` timer: ${eff.timer}`);
21
37
  console.log(` color: ${eff.color}`);
@@ -40,7 +56,11 @@ export async function run({ args }) {
40
56
  }
41
57
  const eff = statuslineDefaults();
42
58
  console.log(`Updated: ${applied.join(', ')}`);
43
- console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel} language=${userLanguage()}`);
59
+ console.log(`Now: labels=${eff.labels} icon-force=${eff.iconForce} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel} language=${userLanguage()}`);
60
+ const { mode: renders } = resolveLabelMode({ cfg: eff });
61
+ if (renders !== eff.labels) {
62
+ console.log(`Renders as: ${renders}${isJetBrainsTerminal() ? ' (IntelliJ detected)' : ''}`);
63
+ }
44
64
  console.log('Statusline picks up the change on the next refresh (~1s).');
45
65
  return;
46
66
  }
@@ -356,11 +356,11 @@ export async function run({ args, hasFlag, numArg }) {
356
356
  }
357
357
  if (lang === 'ko') {
358
358
  lines.push('등록하면 다음 세션부터 자동 위임됩니다. 사용자에게 등록 여부를 물을 때 위 룰 원문을 그대로 보여주고, 적용 범위까지 확인한 뒤 실행하세요:');
359
- lines.push(' sprag harness promote R<N> --project|--global # 적용 범위는 반드시 사용자에게 확인');
359
+ lines.push(' sprag harness promote R<N> --global|--project # 적용 범위는 반드시 사용자에게 확인');
360
360
  lines.push(' sprag route-scan dismiss <N> # 사용자가 원치 않으면');
361
361
  } else {
362
362
  lines.push('Once registered, delegation happens automatically from the next session. Show the user the rule text verbatim, confirm the scope with them, then run:');
363
- lines.push(' sprag harness promote R<N> --project|--global # ALWAYS confirm the scope with the user first');
363
+ lines.push(' sprag harness promote R<N> --global|--project # ALWAYS confirm the scope with the user first');
364
364
  lines.push(' sprag route-scan dismiss <N> # if they do not want it');
365
365
  }
366
366
  }
@@ -446,7 +446,7 @@ export async function run({ args, hasFlag, numArg }) {
446
446
  }
447
447
  console.log('');
448
448
  console.log(lang === 'ko' ? '등록 / 무시:' : 'Promote / dismiss:');
449
- console.log(' sprag harness promote R<N> --project|--global');
449
+ console.log(' sprag harness promote R<N> --global|--project');
450
450
  console.log(' sprag route-scan dismiss <N>');
451
451
  // 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (CTS_NO_NOTE=1 로 끔)
452
452
  (await import('../first-run-note.js')).printOnce('route-scan', lang);
@@ -25,7 +25,8 @@ function runCommand(cmd) {
25
25
  }
26
26
 
27
27
  export async function run({ hasFlag, version }) {
28
- const { updateStatus, refreshUpdateState, upgradeCommand } = await import('../update-check.js');
28
+ const { updateStatus, refreshUpdateState, upgradeCommand, INSTALLED_UNDER_LEGACY_NAME } =
29
+ await import('../update-check.js');
29
30
  const { userLanguage } = await import('../config.js');
30
31
  const lang = userLanguage();
31
32
  const cmd = upgradeCommand();
@@ -40,6 +41,25 @@ export async function run({ hasFlag, version }) {
40
41
  // network round trip is fine here.
41
42
  await refreshUpdateState(version);
42
43
  const s = updateStatus(version);
44
+ // A legacy-name copy has somewhere to go even on the newest version: the
45
+ // package was renamed, and only the new name keeps receiving releases.
46
+ if (!s.available && INSTALLED_UNDER_LEGACY_NAME) {
47
+ console.log(lang === 'ko'
48
+ ? `패키지 이름이 claude-token-saver 에서 sprag-cli 로 바뀌었습니다. 버전은 최신(v${version})이지만, 앞으로의 릴리스를 계속 받으려면 새 이름으로 옮겨야 합니다.`
49
+ : `The package was renamed from claude-token-saver to sprag-cli. You are on the latest version (v${version}), but future releases only ship under the new name.`);
50
+ console.log(`$ ${cmd}`);
51
+ const migrateCode = await runCommand(cmd);
52
+ if (migrateCode !== 0) {
53
+ console.error(lang === 'ko'
54
+ ? `이전 명령이 종료 코드 ${migrateCode}로 실패했습니다. 위 출력을 확인하십시오.`
55
+ : `Migration command failed with exit code ${migrateCode}. Check the output above.`);
56
+ process.exit(migrateCode);
57
+ }
58
+ console.log(lang === 'ko'
59
+ ? '이전이 끝났습니다. 새 셸에서 sprag --version 으로 확인하십시오.'
60
+ : 'Migration done. Confirm with sprag --version in a fresh shell.');
61
+ return;
62
+ }
43
63
  if (!s.available) {
44
64
  console.log(lang === 'ko'
45
65
  ? `이미 최신 버전입니다 (v${version}). 설치할 것이 없습니다.`
@@ -48,8 +68,12 @@ export async function run({ hasFlag, version }) {
48
68
  }
49
69
 
50
70
  console.log(lang === 'ko'
51
- ? `v${version} → ${s.latest} 로 업그레이드합니다.`
52
- : `Upgrading v${version} → ${s.latest}.`);
71
+ ? (INSTALLED_UNDER_LEGACY_NAME
72
+ ? `v${version} → ${s.latest} 로 업그레이드하면서 새 패키지 이름(sprag-cli)으로 함께 옮깁니다.`
73
+ : `v${version} → ${s.latest} 로 업그레이드합니다.`)
74
+ : (INSTALLED_UNDER_LEGACY_NAME
75
+ ? `Upgrading v${version} → ${s.latest} and moving to the new package name (sprag-cli).`
76
+ : `Upgrading v${version} → ${s.latest}.`));
53
77
  console.log(`$ ${cmd}`);
54
78
  const code = await runCommand(cmd);
55
79
  if (code !== 0) {
package/src/config.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { userDataDir } from './paths.js';
15
+ import { labelPreference } from './statusline-mode.js';
15
16
 
16
17
  const CONFIG_DIR = userDataDir();
17
18
  const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
@@ -38,8 +39,17 @@ export function saveConfig(cfg) {
38
39
  // Orthogonal — `mode icon verbose` flips both without resetting the rest.
39
40
  // Statusline-only toggles. Stored under `cfg.statusline`.
40
41
  const KEYWORDS = {
41
- icon: { key: 'icon', value: true },
42
- text: { key: 'icon', value: false },
42
+ // Label style is one setting with three values, so the keywords write one key
43
+ // rather than a pair of booleans that could contradict each other.
44
+ icon: { key: 'labels', value: 'icon' },
45
+ narrow: { key: 'labels', value: 'narrow' },
46
+ text: { key: 'labels', value: 'text' },
47
+ // Opt back into icons inside IntelliJ, where they are downgraded by default
48
+ // for the plugin's statusline widget. Scoped to that guard only: outside
49
+ // IntelliJ it changes nothing, because `icon` / `text` already decide there.
50
+ // See src/statusline-mode.js.
51
+ 'icon-force': { key: 'iconForce', value: true },
52
+ 'no-icon-force': { key: 'iconForce', value: false },
43
53
  verbose: { key: 'verbose', value: true },
44
54
  compact: { key: 'verbose', value: false },
45
55
  timer: { key: 'timer', value: true },
@@ -171,8 +181,17 @@ export function statuslineDefaults() {
171
181
  } else {
172
182
  windowHours = 24; // default: last 1 day
173
183
  }
184
+ // Three-valued label style, with the legacy boolean honored for configs
185
+ // written before it existed. labelPreference() owns that fallback so the
186
+ // resolver and this function cannot disagree about what an old config means.
187
+ const labels = labelPreference(s);
174
188
  return {
175
- icon: s.icon !== false,
189
+ labels,
190
+ // Kept for callers that only ask "emoji or not".
191
+ icon: labels === 'icon',
192
+ // Off unless asked for: narrow labels are the IntelliJ default now, and only
193
+ // the user knows whether they would rather have garbled emoji than glyphs.
194
+ iconForce: s.iconForce === true,
176
195
  verbose: s.verbose !== false,
177
196
  timer: s.timer !== false,
178
197
  color: s.color !== false,