gsd-pi 2.67.0-dev.fe39184 → 2.68.0-dev.4cf2433

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.
Files changed (105) hide show
  1. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  2. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  3. package/dist/resources/extensions/gsd/auto-start.js +5 -31
  4. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  5. package/dist/resources/extensions/gsd/auto.js +94 -59
  6. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  7. package/dist/resources/extensions/gsd/doctor.js +8 -4
  8. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  9. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  10. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  11. package/dist/resources/extensions/gsd/state.js +7 -2
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  14. package/dist/web/standalone/.next/build-manifest.json +3 -3
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  17. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  44. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  50. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  51. package/dist/web/standalone/.next/static/chunks/{webpack-42a66876b763aa26.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  52. package/package.json +1 -1
  53. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  54. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  56. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  60. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  62. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  64. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -0
  72. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/packages/pi-coding-agent/package.json +1 -1
  74. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  75. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  76. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  77. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  78. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +17 -0
  79. package/pkg/package.json +1 -1
  80. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  81. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  82. package/src/resources/extensions/gsd/auto-start.ts +8 -54
  83. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  84. package/src/resources/extensions/gsd/auto.ts +104 -63
  85. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  86. package/src/resources/extensions/gsd/doctor.ts +9 -5
  87. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  88. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  89. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  90. package/src/resources/extensions/gsd/state.ts +7 -1
  91. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  92. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  93. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  94. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  95. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  96. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  97. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  98. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  99. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  100. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  101. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  102. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  103. package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +0 -9
  104. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_buildManifest.js +0 -0
  105. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.67.0",
3
+ "version": "2.68.0",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -0,0 +1,259 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ContextualTips } from "./contextual-tips.js";
4
+
5
+ const baseCtx = {
6
+ input: "hello world",
7
+ isStreaming: false,
8
+ thinkingLevel: "off" as string,
9
+ contextPercent: undefined as number | undefined,
10
+ };
11
+
12
+ describe("ContextualTips", () => {
13
+ describe("shell-command-prefix tip", () => {
14
+ it("fires for bare shell commands", () => {
15
+ const tips = new ContextualTips();
16
+ const result = tips.evaluate({ ...baseCtx, input: "ls -la" });
17
+ assert.ok(result);
18
+ assert.ok(result.includes("looks like a shell command"));
19
+ assert.ok(result.includes("!"));
20
+ });
21
+
22
+ it("fires for various known commands", () => {
23
+ for (const cmd of ["pwd", "cd src", "cat file.txt", "grep foo bar", "git status", "npm install", "docker ps"]) {
24
+ const tips = new ContextualTips();
25
+ const result = tips.evaluate({ ...baseCtx, input: cmd });
26
+ assert.ok(result, `Expected tip for "${cmd}"`);
27
+ assert.ok(result.includes("looks like a shell command"));
28
+ }
29
+ });
30
+
31
+ it("does not fire for commands already prefixed with !", () => {
32
+ const tips = new ContextualTips();
33
+ const result = tips.evaluate({ ...baseCtx, input: "!ls -la" });
34
+ assert.equal(result, null);
35
+ });
36
+
37
+ it("does not fire for commands prefixed with !!", () => {
38
+ const tips = new ContextualTips();
39
+ const result = tips.evaluate({ ...baseCtx, input: "!!ls -la" });
40
+ assert.equal(result, null);
41
+ });
42
+
43
+ it("does not fire for slash commands", () => {
44
+ const tips = new ContextualTips();
45
+ const result = tips.evaluate({ ...baseCtx, input: "/clear" });
46
+ assert.equal(result, null);
47
+ });
48
+
49
+ it("does not fire for unknown commands", () => {
50
+ const tips = new ContextualTips();
51
+ const result = tips.evaluate({ ...baseCtx, input: "please help me fix this bug" });
52
+ assert.equal(result, null);
53
+ });
54
+
55
+ it("does not fire for very long inputs", () => {
56
+ const tips = new ContextualTips();
57
+ const longInput = "ls " + "a".repeat(200);
58
+ const result = tips.evaluate({ ...baseCtx, input: longInput });
59
+ assert.equal(result, null);
60
+ });
61
+
62
+ it("respects maxShows (2)", () => {
63
+ const tips = new ContextualTips();
64
+ tips.evaluate({ ...baseCtx, input: "ls" });
65
+ tips.evaluate({ ...baseCtx, input: "pwd" });
66
+ const third = tips.evaluate({ ...baseCtx, input: "cat foo" });
67
+ assert.equal(third, null);
68
+ });
69
+ });
70
+
71
+ describe("large-paste tip", () => {
72
+ it("fires for large inputs", () => {
73
+ const tips = new ContextualTips();
74
+ const largeInput = "a".repeat(2500);
75
+ const result = tips.evaluate({ ...baseCtx, input: largeInput });
76
+ assert.ok(result);
77
+ assert.ok(result.includes("Large inputs"));
78
+ });
79
+
80
+ it("does not fire for normal-length inputs", () => {
81
+ const tips = new ContextualTips();
82
+ const result = tips.evaluate({ ...baseCtx, input: "fix the login bug" });
83
+ assert.equal(result, null);
84
+ });
85
+
86
+ it("does not fire for large bash commands", () => {
87
+ const tips = new ContextualTips();
88
+ const result = tips.evaluate({ ...baseCtx, input: "!" + "a".repeat(2500) });
89
+ assert.equal(result, null);
90
+ });
91
+
92
+ it("respects maxShows (2)", () => {
93
+ const tips = new ContextualTips();
94
+ const large = "x".repeat(3000);
95
+ tips.evaluate({ ...baseCtx, input: large });
96
+ tips.evaluate({ ...baseCtx, input: large });
97
+ const third = tips.evaluate({ ...baseCtx, input: large });
98
+ assert.equal(third, null);
99
+ });
100
+ });
101
+
102
+ describe("thinking-level-high tip", () => {
103
+ it("fires for short inputs with high thinking", () => {
104
+ const tips = new ContextualTips();
105
+ const result = tips.evaluate({ ...baseCtx, input: "what is 2+2?", thinkingLevel: "high" });
106
+ assert.ok(result);
107
+ assert.ok(result.includes("Thinking is set to high"));
108
+ });
109
+
110
+ it("fires for xhigh thinking", () => {
111
+ const tips = new ContextualTips();
112
+ const result = tips.evaluate({ ...baseCtx, input: "what time is it?", thinkingLevel: "xhigh" });
113
+ assert.ok(result);
114
+ assert.ok(result.includes("Thinking is set to xhigh"));
115
+ });
116
+
117
+ it("does not fire for low/medium thinking", () => {
118
+ const tips = new ContextualTips();
119
+ const result = tips.evaluate({ ...baseCtx, input: "what is 2+2?", thinkingLevel: "medium" });
120
+ assert.equal(result, null);
121
+ });
122
+
123
+ it("does not fire for long inputs", () => {
124
+ const tips = new ContextualTips();
125
+ const longInput = "Please help me refactor this entire authentication module to use JWT tokens instead of session cookies. " +
126
+ "I need to update the middleware, the login handler, and the user model.";
127
+ const result = tips.evaluate({ ...baseCtx, input: longInput, thinkingLevel: "high" });
128
+ assert.equal(result, null);
129
+ });
130
+
131
+ it("does not fire for slash commands", () => {
132
+ const tips = new ContextualTips();
133
+ const result = tips.evaluate({ ...baseCtx, input: "/model", thinkingLevel: "high" });
134
+ assert.equal(result, null);
135
+ });
136
+
137
+ it("respects maxShows (1)", () => {
138
+ const tips = new ContextualTips();
139
+ tips.evaluate({ ...baseCtx, input: "hi", thinkingLevel: "high" });
140
+ const second = tips.evaluate({ ...baseCtx, input: "hello", thinkingLevel: "high" });
141
+ assert.equal(second, null);
142
+ });
143
+ });
144
+
145
+ describe("double-bang-reminder tip", () => {
146
+ it("fires after 3+ included bash commands", () => {
147
+ const tips = new ContextualTips();
148
+ tips.recordBashIncluded();
149
+ tips.recordBashIncluded();
150
+ tips.recordBashIncluded();
151
+ const result = tips.evaluate({ ...baseCtx, input: "!ls" });
152
+ assert.ok(result);
153
+ assert.ok(result.includes("!!"));
154
+ });
155
+
156
+ it("does not fire with fewer than 3 included commands", () => {
157
+ const tips = new ContextualTips();
158
+ tips.recordBashIncluded();
159
+ tips.recordBashIncluded();
160
+ const result = tips.evaluate({ ...baseCtx, input: "!ls" });
161
+ assert.equal(result, null);
162
+ });
163
+
164
+ it("does not fire for !! commands", () => {
165
+ const tips = new ContextualTips();
166
+ tips.recordBashIncluded();
167
+ tips.recordBashIncluded();
168
+ tips.recordBashIncluded();
169
+ const result = tips.evaluate({ ...baseCtx, input: "!!ls" });
170
+ assert.equal(result, null);
171
+ });
172
+
173
+ it("respects maxShows (2)", () => {
174
+ const tips = new ContextualTips();
175
+ for (let i = 0; i < 5; i++) tips.recordBashIncluded();
176
+ tips.evaluate({ ...baseCtx, input: "!ls" });
177
+ tips.evaluate({ ...baseCtx, input: "!pwd" });
178
+ const third = tips.evaluate({ ...baseCtx, input: "!cat foo" });
179
+ assert.equal(third, null);
180
+ });
181
+ });
182
+
183
+ describe("compaction-nudge tip", () => {
184
+ it("fires when context is >= 70%", () => {
185
+ const tips = new ContextualTips();
186
+ const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: 75 });
187
+ assert.ok(result);
188
+ assert.ok(result.includes("/compact"));
189
+ });
190
+
191
+ it("does not fire when context is < 70%", () => {
192
+ const tips = new ContextualTips();
193
+ const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: 50 });
194
+ assert.equal(result, null);
195
+ });
196
+
197
+ it("does not fire when contextPercent is undefined", () => {
198
+ const tips = new ContextualTips();
199
+ const result = tips.evaluate({ ...baseCtx, input: "fix the bug", contextPercent: undefined });
200
+ assert.equal(result, null);
201
+ });
202
+
203
+ it("does not fire for slash commands", () => {
204
+ const tips = new ContextualTips();
205
+ const result = tips.evaluate({ ...baseCtx, input: "/model", contextPercent: 90 });
206
+ assert.equal(result, null);
207
+ });
208
+
209
+ it("respects maxShows (1)", () => {
210
+ const tips = new ContextualTips();
211
+ tips.evaluate({ ...baseCtx, input: "hello", contextPercent: 80 });
212
+ const second = tips.evaluate({ ...baseCtx, input: "world", contextPercent: 85 });
213
+ assert.equal(second, null);
214
+ });
215
+ });
216
+
217
+ describe("reset", () => {
218
+ it("resets all show counters", () => {
219
+ const tips = new ContextualTips();
220
+ // Exhaust shell-command-prefix tip
221
+ tips.evaluate({ ...baseCtx, input: "ls" });
222
+ tips.evaluate({ ...baseCtx, input: "pwd" });
223
+ assert.equal(tips.evaluate({ ...baseCtx, input: "cat foo" }), null);
224
+
225
+ tips.reset();
226
+
227
+ // Should fire again after reset
228
+ const result = tips.evaluate({ ...baseCtx, input: "ls" });
229
+ assert.ok(result);
230
+ assert.ok(result.includes("looks like a shell command"));
231
+ });
232
+
233
+ it("resets bash included count", () => {
234
+ const tips = new ContextualTips();
235
+ for (let i = 0; i < 5; i++) tips.recordBashIncluded();
236
+ assert.equal(tips.bashIncludedCount, 5);
237
+
238
+ tips.reset();
239
+ assert.equal(tips.bashIncludedCount, 0);
240
+ });
241
+ });
242
+
243
+ describe("priority — first match wins", () => {
244
+ it("shell-command-prefix takes priority over compaction nudge", () => {
245
+ const tips = new ContextualTips();
246
+ const result = tips.evaluate({ ...baseCtx, input: "ls", contextPercent: 80 });
247
+ assert.ok(result);
248
+ assert.ok(result.includes("looks like a shell command"));
249
+ });
250
+
251
+ it("large-paste takes priority over compaction nudge", () => {
252
+ const tips = new ContextualTips();
253
+ const largeInput = "x".repeat(3000);
254
+ const result = tips.evaluate({ ...baseCtx, input: largeInput, contextPercent: 80 });
255
+ assert.ok(result);
256
+ assert.ok(result.includes("Large inputs"));
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Contextual tips system — shows non-intrusive, session-scoped hints
3
+ * when user behavior suggests they'd benefit from knowing a feature.
4
+ *
5
+ * Each tip fires at most `maxShows` times per session. Tips are
6
+ * evaluated in order; the first match wins per input event.
7
+ */
8
+
9
+ // ─── Tip definitions ─────────────────────────────────────────────────────────
10
+
11
+ export interface TipContext {
12
+ /** The raw input text the user submitted */
13
+ input: string;
14
+ /** Whether the agent is currently streaming */
15
+ isStreaming: boolean;
16
+ /** Current thinking level (e.g. "off", "low", "high", "xhigh") */
17
+ thinkingLevel?: string;
18
+ /** Number of `!` (included) bash commands run this session */
19
+ bashIncludedCount: number;
20
+ /** Approximate context usage percentage (0–100), if known */
21
+ contextPercent?: number;
22
+ }
23
+
24
+ export interface Tip {
25
+ id: string;
26
+ /** Maximum times this tip is shown per session */
27
+ maxShows: number;
28
+ /** Returns the tip message if the tip should fire, or null to skip */
29
+ evaluate: (ctx: TipContext) => string | null;
30
+ }
31
+
32
+ // Shell commands that obviously run locally and don't need the LLM.
33
+ // Intentionally conservative — these are unambiguous filesystem/info commands.
34
+ const LOCAL_SHELL_COMMANDS = new Set([
35
+ "ls",
36
+ "ll",
37
+ "la",
38
+ "pwd",
39
+ "cd",
40
+ "dir",
41
+ "cat",
42
+ "head",
43
+ "tail",
44
+ "wc",
45
+ "file",
46
+ "which",
47
+ "whoami",
48
+ "echo",
49
+ "date",
50
+ "tree",
51
+ "find",
52
+ "grep",
53
+ "rg",
54
+ "clear",
55
+ "env",
56
+ "df",
57
+ "du",
58
+ "uname",
59
+ "hostname",
60
+ "mkdir",
61
+ "rm",
62
+ "cp",
63
+ "mv",
64
+ "touch",
65
+ "chmod",
66
+ "less",
67
+ "more",
68
+ "sort",
69
+ "uniq",
70
+ "sed",
71
+ "awk",
72
+ "curl",
73
+ "wget",
74
+ "tar",
75
+ "zip",
76
+ "unzip",
77
+ "git",
78
+ "docker",
79
+ "npm",
80
+ "npx",
81
+ "yarn",
82
+ "pnpm",
83
+ "node",
84
+ "python",
85
+ "python3",
86
+ "pip",
87
+ "pip3",
88
+ "make",
89
+ "cargo",
90
+ "go",
91
+ "ruby",
92
+ "brew",
93
+ ]);
94
+
95
+ /**
96
+ * Extract the first token from input, ignoring leading whitespace.
97
+ * Returns lowercase for case-insensitive matching.
98
+ */
99
+ function firstToken(input: string): string {
100
+ const trimmed = input.trimStart();
101
+ const spaceIdx = trimmed.search(/\s/);
102
+ const token = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx);
103
+ return token.toLowerCase();
104
+ }
105
+
106
+ /**
107
+ * Check if input looks like a bare shell command (no !, //, or slash prefix).
108
+ */
109
+ function looksLikeShellCommand(input: string): boolean {
110
+ const trimmed = input.trimStart();
111
+ // Already prefixed — user knows what they're doing
112
+ if (trimmed.startsWith("!") || trimmed.startsWith("/")) return false;
113
+ // Multi-line or very long inputs are probably prompts
114
+ if (trimmed.includes("\n") || trimmed.length > 120) return false;
115
+ return LOCAL_SHELL_COMMANDS.has(firstToken(trimmed));
116
+ }
117
+
118
+ const TIPS: Tip[] = [
119
+ // 1. Shell command reminder
120
+ {
121
+ id: "shell-command-prefix",
122
+ maxShows: 2,
123
+ evaluate(ctx) {
124
+ if (!looksLikeShellCommand(ctx.input)) return null;
125
+ const cmd = firstToken(ctx.input);
126
+ return `Tip: "${cmd}" looks like a shell command. Prefix with ! to run locally, or !! to run without using tokens.`;
127
+ },
128
+ },
129
+
130
+ // 2. Large paste warning
131
+ {
132
+ id: "large-paste",
133
+ maxShows: 2,
134
+ evaluate(ctx) {
135
+ if (ctx.input.length < 2000) return null;
136
+ // Slash commands and bash prefixes are intentional
137
+ if (ctx.input.trimStart().startsWith("/") || ctx.input.trimStart().startsWith("!")) return null;
138
+ return "Tip: Large inputs consume many tokens. Consider saving to a file and asking the agent to read it.";
139
+ },
140
+ },
141
+
142
+ // 3. Thinking level awareness
143
+ {
144
+ id: "thinking-level-high",
145
+ maxShows: 1,
146
+ evaluate(ctx) {
147
+ const level = ctx.thinkingLevel?.toLowerCase();
148
+ if (level !== "high" && level !== "xhigh") return null;
149
+ // Only fire for short, simple-looking inputs (likely simple questions)
150
+ const trimmed = ctx.input.trim();
151
+ if (trimmed.length > 80 || trimmed.includes("\n")) return null;
152
+ // Don't fire on slash or bash commands
153
+ if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null;
154
+ return `Tip: Thinking is set to ${level}. Use Ctrl+T to lower it for simple questions — saves tokens.`;
155
+ },
156
+ },
157
+
158
+ // 4. Double-bang reminder
159
+ {
160
+ id: "double-bang-reminder",
161
+ maxShows: 2,
162
+ evaluate(ctx) {
163
+ // Fire after user has run 3+ included (!) bash commands
164
+ if (ctx.bashIncludedCount < 3) return null;
165
+ // Only trigger on a ! command (not !!)
166
+ const trimmed = ctx.input.trimStart();
167
+ if (!trimmed.startsWith("!") || trimmed.startsWith("!!")) return null;
168
+ return "Tip: Use !! instead of ! to keep command output out of agent context and save tokens.";
169
+ },
170
+ },
171
+
172
+ // 5. Compaction nudge
173
+ {
174
+ id: "compaction-nudge",
175
+ maxShows: 1,
176
+ evaluate(ctx) {
177
+ if (ctx.contextPercent === undefined || ctx.contextPercent < 70) return null;
178
+ // Don't nag on slash/bash
179
+ const trimmed = ctx.input.trimStart();
180
+ if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null;
181
+ return "Tip: Context is getting full. Use /compact to summarize the conversation and free up space.";
182
+ },
183
+ },
184
+ ];
185
+
186
+ // ─── Session-scoped tracker ──────────────────────────────────────────────────
187
+
188
+ export class ContextualTips {
189
+ /** Map of tip ID → number of times shown this session */
190
+ private showCounts = new Map<string, number>();
191
+ /** Track ! bash commands for double-bang reminder */
192
+ private _bashIncludedCount = 0;
193
+
194
+ /** Increment the bash-included counter. Call when user runs ! (not !!) command. */
195
+ recordBashIncluded(): void {
196
+ this._bashIncludedCount++;
197
+ }
198
+
199
+ get bashIncludedCount(): number {
200
+ return this._bashIncludedCount;
201
+ }
202
+
203
+ /**
204
+ * Evaluate all tips against the current input context.
205
+ * Returns the first matching tip message, or null if none apply.
206
+ */
207
+ evaluate(ctx: Omit<TipContext, "bashIncludedCount">): string | null {
208
+ const fullCtx: TipContext = {
209
+ ...ctx,
210
+ bashIncludedCount: this._bashIncludedCount,
211
+ };
212
+
213
+ for (const tip of TIPS) {
214
+ const shown = this.showCounts.get(tip.id) ?? 0;
215
+ if (shown >= tip.maxShows) continue;
216
+
217
+ const message = tip.evaluate(fullCtx);
218
+ if (message) {
219
+ this.showCounts.set(tip.id, shown + 1);
220
+ return message;
221
+ }
222
+ }
223
+
224
+ return null;
225
+ }
226
+
227
+ /** Reset all counters (e.g. on new session). */
228
+ reset(): void {
229
+ this.showCounts.clear();
230
+ this._bashIncludedCount = 0;
231
+ }
232
+ }
@@ -66,3 +66,5 @@ export {
66
66
  type TurnStartEvent,
67
67
  wrapToolsWithExtensions,
68
68
  } from "./extensions/index.js";
69
+
70
+ export { ContextualTips, type TipContext } from "./contextual-tips.js";
@@ -1,17 +1,21 @@
1
1
  import { dispatchSlashCommand } from "../slash-command-handlers.js";
2
2
  import type { InteractiveModeStateHost } from "../interactive-mode-state.js";
3
+ import type { ContextualTips } from "../../../core/contextual-tips.js";
3
4
 
4
5
  export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
5
6
  getSlashCommandContext: () => any;
6
7
  handleBashCommand: (command: string, excludeFromContext?: boolean) => Promise<void>;
7
8
  showWarning: (message: string) => void;
8
9
  showError: (message: string) => void;
10
+ showTip: (message: string) => void;
9
11
  updateEditorBorderColor: () => void;
10
12
  isExtensionCommand: (text: string) => boolean;
11
13
  isKnownSlashCommand: (text: string) => boolean;
12
14
  queueCompactionMessage: (text: string, mode: "steer" | "followUp") => void;
13
15
  updatePendingMessagesDisplay: () => void;
14
16
  flushPendingBashComponents: () => void;
17
+ contextualTips: ContextualTips;
18
+ getContextPercent: () => number | undefined;
15
19
  options?: { submitPromptsDirectly?: boolean };
16
20
  }): void {
17
21
  host.defaultEditor.onSubmit = async (text: string) => {
@@ -41,6 +45,10 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
41
45
  host.editor.setText(text);
42
46
  return;
43
47
  }
48
+ // Track included bash commands for double-bang tip
49
+ if (!isExcluded) {
50
+ host.contextualTips.recordBashIncluded();
51
+ }
44
52
  host.editor.addToHistory?.(text);
45
53
  await host.handleBashCommand(command, isExcluded);
46
54
  host.isBashMode = false;
@@ -49,6 +57,17 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
49
57
  }
50
58
  }
51
59
 
60
+ // Evaluate contextual tips before sending to agent
61
+ const tip = host.contextualTips.evaluate({
62
+ input: text,
63
+ isStreaming: host.session.isStreaming,
64
+ thinkingLevel: host.session.thinkingLevel,
65
+ contextPercent: host.getContextPercent(),
66
+ });
67
+ if (tip) {
68
+ host.showTip(tip);
69
+ }
70
+
52
71
  if (host.session.isCompacting) {
53
72
  if (host.isExtensionCommand(text)) {
54
73
  host.editor.addToHistory?.(text);
@@ -90,6 +90,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
90
90
  import { TreeSelectorComponent } from "./components/tree-selector.js";
91
91
  import { UserMessageComponent } from "./components/user-message.js";
92
92
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
93
+ import { ContextualTips } from "../../core/contextual-tips.js";
93
94
  import { type SlashCommandContext, dispatchSlashCommand, getAppKeyDisplay } from "./slash-command-handlers.js";
94
95
  import { handleAgentEvent } from "./controllers/chat-controller.js";
95
96
  import { createExtensionUIContext as buildExtensionUIContext } from "./controllers/extension-ui-controller.js";
@@ -214,6 +215,9 @@ export class InteractiveMode {
214
215
  // Track if editor is in bash mode (text starts with !)
215
216
  private isBashMode = false;
216
217
 
218
+ // Contextual tips — session-scoped, non-intrusive hints
219
+ private contextualTips = new ContextualTips();
220
+
217
221
  // Track current bash execution component
218
222
  private bashComponent: BashExecutionComponent | undefined = undefined;
219
223
 
@@ -2593,6 +2597,16 @@ export class InteractiveMode {
2593
2597
  this.ui.requestRender();
2594
2598
  }
2595
2599
 
2600
+ showTip(message: string): void {
2601
+ this.chatContainer.addChild(new Spacer(1));
2602
+ this.chatContainer.addChild(new Text(theme.fg("dim", `💡 ${message}`), 1, 0));
2603
+ this.ui.requestRender();
2604
+ }
2605
+
2606
+ getContextPercent(): number | undefined {
2607
+ return this.session.getContextUsage()?.percent ?? undefined;
2608
+ }
2609
+
2596
2610
  showNewVersionNotification(newVersion: string): void {
2597
2611
  const action = theme.fg("accent", getUpdateInstruction("@gsd/pi-coding-agent"));
2598
2612
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
@@ -3679,6 +3693,9 @@ export class InteractiveMode {
3679
3693
  this.streamingMessage = undefined;
3680
3694
  this.pendingTools.clear();
3681
3695
 
3696
+ // Reset contextual tips for the new session
3697
+ this.contextualTips.reset();
3698
+
3682
3699
  this.chatContainer.addChild(new Spacer(1));
3683
3700
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
3684
3701
  this.ui.requestRender();
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.67.0",
3
+ "version": "2.68.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -119,6 +119,8 @@ export class AutoSession {
119
119
  pendingVerificationRetry: PendingVerificationRetry | null = null;
120
120
  readonly verificationRetryCount = new Map<string, number>();
121
121
  pausedSessionFile: string | null = null;
122
+ pausedUnitType: string | null = null;
123
+ pausedUnitId: string | null = null;
122
124
  resourceVersionOnStart: string | null = null;
123
125
  lastStateRebuildAt = 0;
124
126
 
@@ -223,6 +225,8 @@ export class AutoSession {
223
225
  this.pendingVerificationRetry = null;
224
226
  this.verificationRetryCount.clear();
225
227
  this.pausedSessionFile = null;
228
+ this.pausedUnitType = null;
229
+ this.pausedUnitId = null;
226
230
  this.resourceVersionOnStart = null;
227
231
  this.lastStateRebuildAt = 0;
228
232
 
@@ -178,7 +178,7 @@ export function incrementUatCount(basePath: string, mid: string, sid: string): n
178
178
  export function isVerificationNotApplicable(value: string): boolean {
179
179
  const v = (value ?? "").toLowerCase().trim().replace(/[.\s]+$/, "");
180
180
  if (!v || v === "none") return true;
181
- return /^(?:none[\s._-]*(?:required|needed|planned)?|n\/?a|not[\s._-]+(?:applicable|required|needed|provided)|no[\s._-]+operational[\s\S]*)$/i.test(v);
181
+ return /^(?:none(?:[\s._\u2014-]+[\s\S]*)?|n\/?a|not[\s._-]+(?:applicable|required|needed|provided)|no[\s._-]+operational[\s\S]*)$/i.test(v);
182
182
  }
183
183
 
184
184
  // ─── Rules ────────────────────────────────────────────────────────────────