@delt/claude-jev-advisor 0.3.0 → 0.4.1

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.
@@ -13,6 +13,7 @@ var DEFAULT_CONFIG = {
13
13
  lang: "ko",
14
14
  keyFile: null,
15
15
  display: "mod",
16
+ shortcut: null,
16
17
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
17
18
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
18
19
  };
@@ -40,6 +41,7 @@ function normalizeConfig(raw) {
40
41
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
41
42
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
42
43
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
44
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
43
45
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
44
46
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
45
47
  };
@@ -63,18 +65,20 @@ function readConfig(home) {
63
65
  }
64
66
 
65
67
  // src/display/line.ts
66
- var PHRASES = {
68
+ var QUESTIONS = {
67
69
  ko: {
68
- clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
69
- compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
70
- compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
70
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
71
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
72
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?"
71
73
  },
72
74
  en: {
73
- clear: "Start fresh? /clear",
74
- compact: "Wrap up what you have and continue? /compact",
75
- compactLater: "When this work is done, wrap up and continue? /compact"
75
+ clear: "Start fresh?",
76
+ compact: "Wrap up what you have and continue?",
77
+ compactLater: "When this work is done, wrap up and continue?"
76
78
  }
77
79
  };
80
+ var COMMANDS = { clear: "/clear", compact: "/compact", compactLater: "/compact" };
81
+ var phrase = (lang, advice) => `${QUESTIONS[lang][advice]} ${COMMANDS[advice]}`;
78
82
  function formatSize(tokens) {
79
83
  return `${Math.round(tokens / 1e3)}k`;
80
84
  }
@@ -86,20 +90,29 @@ function adviceKind(size, judgment, config) {
86
90
  if (judgment.clear) return "clear";
87
91
  return size >= config.context.compactMinTokens ? "compact" : null;
88
92
  }
89
- function adviceLine({ size, threshold, judgment, config }) {
93
+ function increaseSuffix(increase) {
94
+ if (increase === null || increase === void 0) return "";
95
+ const k = Math.round(increase / 1e3);
96
+ return k > 0 ? ` +${k}k` : "";
97
+ }
98
+ function redRemaining(size, threshold, config) {
99
+ if (threshold === null || threshold <= 0 || size < threshold * (1 - config.context.redRemainingPct / 100)) return null;
100
+ return remainingPct(size, threshold);
101
+ }
102
+ function adviceLine({ size, threshold, judgment, config, increase }) {
90
103
  if (size === null) return "";
91
- const k = formatSize(size);
92
- const phrases = PHRASES[config.lang];
104
+ const k = `${formatSize(size)}${increaseSuffix(increase)}`;
93
105
  const counted = size >= config.context.minTokens ? judgment : null;
94
- if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
95
- const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
106
+ const red = redRemaining(size, threshold, config);
107
+ if (red !== null) {
108
+ const head = `\u{1F534} ${k} ${red}%`;
96
109
  if (!counted) return head;
97
- if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
98
- return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
110
+ if (counted.phase === "working") return `${head} \xB7 ${phrase(config.lang, "compactLater")}`;
111
+ return `${head} \xB7 ${phrase(config.lang, counted.clear ? "clear" : "compact")}`;
99
112
  }
100
113
  if (!counted) return k;
101
114
  const kind = adviceKind(size, counted, config);
102
- return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
115
+ return kind ? `\u{1F7E1} ${k} ${phrase(config.lang, kind)}` : `\u{1F7E2} ${k}`;
103
116
  }
104
117
 
105
118
  // src/jev.ts
@@ -225,7 +238,7 @@ function appendLog(home, now, record, secrets = []) {
225
238
 
226
239
  // src/context/judge.ts
227
240
  var REPLY_EDGE_CHARS = 1500;
228
- var QUESTIONS = {
241
+ var QUESTIONS2 = {
229
242
  unit_done: {
230
243
  type: "noul",
231
244
  instructions: "Has Claude finished or handed back the work the user asked for in `recent_requests`, so that `last_assistant_reply` is a natural break before the user's next instruction?",
@@ -259,7 +272,7 @@ ${text.slice(-edge)}`;
259
272
  function jevRequest(requests, lastReply) {
260
273
  return {
261
274
  state: { recent_requests: requests, last_assistant_reply: cutReply(lastReply) },
262
- questions: { unit_done: QUESTIONS.unit_done, phase_done: QUESTIONS.phase_done }
275
+ questions: { unit_done: QUESTIONS2.unit_done, phase_done: QUESTIONS2.phase_done }
263
276
  };
264
277
  }
265
278
  function judgmentFrom(answers, config) {
package/dist/rm-hook.js CHANGED
@@ -452,6 +452,7 @@ var DEFAULT_CONFIG = {
452
452
  lang: "ko",
453
453
  keyFile: null,
454
454
  display: "mod",
455
+ shortcut: null,
455
456
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
456
457
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
457
458
  };
@@ -479,6 +480,7 @@ function normalizeConfig(raw) {
479
480
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
480
481
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
481
482
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
483
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
482
484
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
483
485
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
484
486
  };
@@ -18,6 +18,7 @@ var DEFAULT_CONFIG = {
18
18
  lang: "ko",
19
19
  keyFile: null,
20
20
  display: "mod",
21
+ shortcut: null,
21
22
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
22
23
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
23
24
  };
@@ -45,6 +46,7 @@ function normalizeConfig(raw) {
45
46
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
46
47
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
47
48
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
49
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
48
50
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
49
51
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
50
52
  };
@@ -73,18 +75,20 @@ import path3 from "path";
73
75
 
74
76
  // src/display/line.ts
75
77
  var STALE_SIZE_RATIO = 0.7;
76
- var PHRASES = {
78
+ var QUESTIONS = {
77
79
  ko: {
78
- clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
79
- compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
80
- compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
80
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
81
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
82
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?"
81
83
  },
82
84
  en: {
83
- clear: "Start fresh? /clear",
84
- compact: "Wrap up what you have and continue? /compact",
85
- compactLater: "When this work is done, wrap up and continue? /compact"
85
+ clear: "Start fresh?",
86
+ compact: "Wrap up what you have and continue?",
87
+ compactLater: "When this work is done, wrap up and continue?"
86
88
  }
87
89
  };
90
+ var COMMANDS = { clear: "/clear", compact: "/compact", compactLater: "/compact" };
91
+ var phrase = (lang, advice) => `${QUESTIONS[lang][advice]} ${COMMANDS[advice]}`;
88
92
  function isRecord2(value) {
89
93
  return typeof value === "object" && value !== null && !Array.isArray(value);
90
94
  }
@@ -99,20 +103,29 @@ function adviceKind(size, judgment, config) {
99
103
  if (judgment.clear) return "clear";
100
104
  return size >= config.context.compactMinTokens ? "compact" : null;
101
105
  }
102
- function adviceLine({ size, threshold, judgment, config }) {
106
+ function increaseSuffix(increase) {
107
+ if (increase === null || increase === void 0) return "";
108
+ const k = Math.round(increase / 1e3);
109
+ return k > 0 ? ` +${k}k` : "";
110
+ }
111
+ function redRemaining(size, threshold, config) {
112
+ if (threshold === null || threshold <= 0 || size < threshold * (1 - config.context.redRemainingPct / 100)) return null;
113
+ return remainingPct(size, threshold);
114
+ }
115
+ function adviceLine({ size, threshold, judgment, config, increase }) {
103
116
  if (size === null) return "";
104
- const k = formatSize(size);
105
- const phrases = PHRASES[config.lang];
117
+ const k = `${formatSize(size)}${increaseSuffix(increase)}`;
106
118
  const counted = size >= config.context.minTokens ? judgment : null;
107
- if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
108
- const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
119
+ const red = redRemaining(size, threshold, config);
120
+ if (red !== null) {
121
+ const head = `\u{1F534} ${k} ${red}%`;
109
122
  if (!counted) return head;
110
- if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
111
- return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
123
+ if (counted.phase === "working") return `${head} \xB7 ${phrase(config.lang, "compactLater")}`;
124
+ return `${head} \xB7 ${phrase(config.lang, counted.clear ? "clear" : "compact")}`;
112
125
  }
113
126
  if (!counted) return k;
114
127
  const kind = adviceKind(size, counted, config);
115
- return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
128
+ return kind ? `\u{1F7E1} ${k} ${phrase(config.lang, kind)}` : `\u{1F7E2} ${k}`;
116
129
  }
117
130
  function usableJudgment(state, liveSize, turnStartedAt) {
118
131
  if (!state || !state.judgment || state.at < turnStartedAt) return null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jev-advisor",
3
- "version": "0.3.0",
4
- "description": "Bottom-row compact/clear advice from @delt/claude-jev-advisor (unofficial)",
3
+ "version": "0.4.1",
4
+ "description": "Bottom-row compact/clear advice and an advice button from @delt/claude-jev-advisor (unofficial)",
5
5
  "author": { "name": "delt96" },
6
6
  "userConfig": {
7
7
  "dataDir": {
@@ -4,6 +4,7 @@ var DEFAULT_CONFIG = {
4
4
  lang: "ko",
5
5
  keyFile: null,
6
6
  display: "mod",
7
+ shortcut: null,
7
8
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
8
9
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
9
10
  };
@@ -31,6 +32,7 @@ function normalizeConfig(raw) {
31
32
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
32
33
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
33
34
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
35
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
34
36
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
35
37
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
36
38
  };
@@ -38,21 +40,26 @@ function normalizeConfig(raw) {
38
40
 
39
41
  // src/display/line.ts
40
42
  var STALE_SIZE_RATIO = 0.7;
41
- var PHRASES = {
43
+ var QUESTIONS = {
42
44
  ko: {
43
- clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
44
- compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
45
- compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
45
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
46
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
47
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?"
46
48
  },
47
49
  en: {
48
- clear: "Start fresh? /clear",
49
- compact: "Wrap up what you have and continue? /compact",
50
- compactLater: "When this work is done, wrap up and continue? /compact"
50
+ clear: "Start fresh?",
51
+ compact: "Wrap up what you have and continue?",
52
+ compactLater: "When this work is done, wrap up and continue?"
51
53
  }
52
54
  };
55
+ var COMMANDS = { clear: "/clear", compact: "/compact", compactLater: "/compact" };
56
+ var phrase = (lang, advice) => `${QUESTIONS[lang][advice]} ${COMMANDS[advice]}`;
53
57
  function isRecord2(value) {
54
58
  return typeof value === "object" && value !== null && !Array.isArray(value);
55
59
  }
60
+ function adviceQuestion(kind, lang) {
61
+ return QUESTIONS[lang][kind];
62
+ }
56
63
  function formatSize(tokens) {
57
64
  return `${Math.round(tokens / 1e3)}k`;
58
65
  }
@@ -64,20 +71,42 @@ function adviceKind(size, judgment, config) {
64
71
  if (judgment.clear) return "clear";
65
72
  return size >= config.context.compactMinTokens ? "compact" : null;
66
73
  }
67
- function adviceLine({ size, threshold, judgment, config }) {
74
+ function increaseSuffix(increase) {
75
+ if (increase === null || increase === void 0) return "";
76
+ const k = Math.round(increase / 1e3);
77
+ return k > 0 ? ` +${k}k` : "";
78
+ }
79
+ function redRemaining(size, threshold, config) {
80
+ if (threshold === null || threshold <= 0 || size < threshold * (1 - config.context.redRemainingPct / 100)) return null;
81
+ return remainingPct(size, threshold);
82
+ }
83
+ function adviceLine({ size, threshold, judgment, config, increase }) {
68
84
  if (size === null) return "";
69
- const k = formatSize(size);
70
- const phrases = PHRASES[config.lang];
85
+ const k = `${formatSize(size)}${increaseSuffix(increase)}`;
71
86
  const counted = size >= config.context.minTokens ? judgment : null;
72
- if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
73
- const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
87
+ const red = redRemaining(size, threshold, config);
88
+ if (red !== null) {
89
+ const head = `\u{1F534} ${k} ${red}%`;
74
90
  if (!counted) return head;
75
- if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
76
- return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
91
+ if (counted.phase === "working") return `${head} \xB7 ${phrase(config.lang, "compactLater")}`;
92
+ return `${head} \xB7 ${phrase(config.lang, counted.clear ? "clear" : "compact")}`;
77
93
  }
78
94
  if (!counted) return k;
79
95
  const kind = adviceKind(size, counted, config);
80
- return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
96
+ return kind ? `\u{1F7E1} ${k} ${phrase(config.lang, kind)}` : `\u{1F7E2} ${k}`;
97
+ }
98
+ function bandAdvice({ size, threshold, judgment, config }) {
99
+ if (size === null) return null;
100
+ const counted = size >= config.context.minTokens ? judgment : null;
101
+ if (!counted || counted.phase === "working") return null;
102
+ const red = redRemaining(size, threshold, config);
103
+ if (red !== null) return { kind: counted.clear ? "clear" : "compact", remainingPct: red };
104
+ const kind = adviceKind(size, counted, config);
105
+ return kind ? { kind, remainingPct: null } : null;
106
+ }
107
+ function modTail(input) {
108
+ if (input.size === null) return "";
109
+ return bandAdvice(input) ? `${formatSize(input.size)}${increaseSuffix(input.increase)}` : adviceLine(input);
81
110
  }
82
111
  function usableJudgment(state, liveSize2, turnStartedAt) {
83
112
  if (!state || !state.judgment || state.at < turnStartedAt) return null;
@@ -91,6 +120,44 @@ function parseState(raw) {
91
120
  return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
92
121
  }
93
122
 
123
+ // src/display/shortcut.ts
124
+ var SHORTCUT_ACTION = "diff:back";
125
+
126
+ // src/mod/band.ts
127
+ var ICONS = { compact: "\u{1F4E6}", clear: "\u{1F9F9}" };
128
+ function drawBand(ui, { advice, config, onPress, size, increase }) {
129
+ const { Box, Text, Button } = ui;
130
+ const k = `${formatSize(size)}${increaseSuffix(increase)}`;
131
+ const head = advice.remainingPct === null ? `\u{1F7E1} ${k}` : `\u{1F534} ${k} ${advice.remainingPct}%`;
132
+ const controls = [
133
+ h(Button, {
134
+ key: `jev-${advice.kind}`,
135
+ label: `${ICONS[advice.kind]} /${advice.kind}`,
136
+ variant: "primary",
137
+ autoFocus: true,
138
+ hover: { bold: true },
139
+ onPress,
140
+ ...config.shortcut ? { action: SHORTCUT_ACTION } : {}
141
+ })
142
+ ];
143
+ if (config.shortcut) controls.push(h(Text, { dimColor: true }, config.shortcut));
144
+ return h(
145
+ Box,
146
+ { flexDirection: "row", flexWrap: "wrap", columnGap: 2, paddingTop: 1 },
147
+ h(
148
+ Box,
149
+ { flexDirection: "row", gap: 1, minWidth: 0, flexShrink: 1 },
150
+ h(Box, { flexShrink: 0 }, h(Text, { bold: advice.remainingPct !== null }, head)),
151
+ h(
152
+ Box,
153
+ { minWidth: 0, flexShrink: 1 },
154
+ h(Text, { wrap: "truncate-end" }, adviceQuestion(advice.kind, config.lang))
155
+ )
156
+ ),
157
+ h(Box, { key: `jev-${advice.kind}-control`, flexDirection: "row", gap: 1, flexShrink: 0 }, ...controls)
158
+ );
159
+ }
160
+
94
161
  // src/mod/register.ts
95
162
  var REFRESH_MS = 2e3;
96
163
  async function readJson($, file) {
@@ -130,13 +197,37 @@ async function liveSize($) {
130
197
  return null;
131
198
  }
132
199
  }
200
+ function reason(err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ return message.replace(/^[^:]+: \$\.[\w]+(?:\.[\w]+)*: /, "");
203
+ }
204
+ async function press($, kind) {
205
+ try {
206
+ $.ui.invalidate("ui.render");
207
+ if (kind === "compact") {
208
+ const result = await $.session.compact();
209
+ if (result?.skip) $.ui.toast("/compact: skipped by a hook");
210
+ } else {
211
+ await $.command.run({ command: "clear" });
212
+ }
213
+ } catch (err) {
214
+ try {
215
+ $.ui.toast(`/${kind}: ${reason(err)}`);
216
+ } catch {
217
+ }
218
+ }
219
+ }
133
220
  function register(on, options) {
134
221
  const dataDir = typeof options.dataDir === "string" ? options.dataDir.replace(/[\\/]+$/, "") : "";
135
222
  let config = null;
136
223
  let state = null;
137
224
  let threshold = null;
138
225
  let turnStartedAt = 0;
226
+ let baseline = null;
139
227
  let seen = "";
228
+ let handledAt = -Infinity;
229
+ const increaseTo = (size) => baseline !== null && size !== null ? size - baseline : null;
230
+ const judgmentFor = (size) => state && state.at <= handledAt ? null : usableJudgment(state, size, turnStartedAt);
140
231
  on("session.start", async ($, e, next) => {
141
232
  const result = await next(e);
142
233
  if (!dataDir || e.isInteractive === false) return result;
@@ -159,8 +250,10 @@ function register(on, options) {
159
250
  return result;
160
251
  });
161
252
  on("turn.start", async ($, e, next) => {
253
+ baseline = null;
162
254
  try {
163
255
  turnStartedAt = await $.clock.now();
256
+ baseline = await liveSize($);
164
257
  $.ui.invalidate("ui.render");
165
258
  } catch {
166
259
  }
@@ -171,19 +264,49 @@ function register(on, options) {
171
264
  threshold = await autoCompactThreshold($);
172
265
  return result;
173
266
  });
267
+ on("session.end", async ($, e, next) => {
268
+ baseline = null;
269
+ state = null;
270
+ seen = "";
271
+ try {
272
+ $.ui.invalidate("ui.render");
273
+ } catch {
274
+ }
275
+ return next(e);
276
+ });
174
277
  on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
175
278
  let tail = "";
176
279
  try {
177
280
  const current = config;
178
281
  if (current && current.context.enabled && current.display === "mod") {
179
282
  const size = await liveSize($);
180
- tail = adviceLine({ size, threshold, judgment: usableJudgment(state, size, turnStartedAt), config: current });
283
+ tail = modTail({ size, threshold, judgment: judgmentFor(size), config: current, increase: increaseTo(size) });
181
284
  }
182
285
  } catch {
183
286
  tail = "";
184
287
  }
185
288
  return tail ? next({ ...e, props: { ...e.props, tail } }) : next(e);
186
289
  });
290
+ on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
291
+ let band = null;
292
+ try {
293
+ const current = config;
294
+ const props = e.props ?? {};
295
+ if (current && current.context.enabled && current.display === "mod" && props.isWorking !== true && props.hasSurvey !== true) {
296
+ const size = await liveSize($);
297
+ const advice = bandAdvice({ size, threshold, judgment: judgmentFor(size), config: current });
298
+ const at = state?.at;
299
+ if (advice && at !== void 0 && size !== null) band = drawBand($.ui.resolve(e), { advice, config: current, size, increase: increaseTo(size), onPress: () => {
300
+ if (at <= handledAt) return;
301
+ handledAt = at;
302
+ void press($, advice.kind);
303
+ } });
304
+ }
305
+ } catch {
306
+ band = null;
307
+ }
308
+ return band ?? next(e);
309
+ });
187
310
  }
188
311
  export {
189
312
  register
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delt/claude-jev-advisor",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Unofficial Claude Code helpers using TypeSafe Jev: compact/clear advice and an rm guard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "mod",
11
+ "mod/.claude-plugin/plugin.json",
12
+ "mod/hooks",
12
13
  "README.md"
13
14
  ],
14
15
  "engines": {