@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.3

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 (177) hide show
  1. package/CHANGELOG.md +95 -6
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/cli/extension-flags.d.ts +36 -0
  5. package/dist/types/config/config-file.d.ts +4 -0
  6. package/dist/types/config/file-lock.d.ts +23 -0
  7. package/dist/types/config/keybindings.d.ts +2 -1
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/settings-schema.d.ts +112 -69
  10. package/dist/types/edit/hashline/diff.d.ts +9 -3
  11. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  13. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  14. package/dist/types/eval/agent-bridge.d.ts +25 -0
  15. package/dist/types/eval/backend.d.ts +17 -2
  16. package/dist/types/eval/budget-bridge.d.ts +29 -0
  17. package/dist/types/eval/idle-timeout.d.ts +28 -0
  18. package/dist/types/eval/js/executor.d.ts +8 -0
  19. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  20. package/dist/types/eval/py/executor.d.ts +13 -0
  21. package/dist/types/exec/bash-executor.d.ts +1 -0
  22. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  23. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  24. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  25. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  26. package/dist/types/extensibility/shared-events.d.ts +2 -2
  27. package/dist/types/memory-backend/index.d.ts +1 -1
  28. package/dist/types/memory-backend/resolve.d.ts +1 -1
  29. package/dist/types/memory-backend/types.d.ts +3 -3
  30. package/dist/types/mnemopi/backend.d.ts +4 -0
  31. package/dist/types/mnemopi/config.d.ts +29 -0
  32. package/dist/types/mnemopi/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  34. package/dist/types/modes/components/model-selector.d.ts +3 -2
  35. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  36. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  37. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  38. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  39. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  40. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  41. package/dist/types/modes/interactive-mode.d.ts +7 -3
  42. package/dist/types/modes/magic-keywords.d.ts +14 -0
  43. package/dist/types/modes/markdown-prose.d.ts +27 -0
  44. package/dist/types/modes/orchestrate.d.ts +7 -2
  45. package/dist/types/modes/shared.d.ts +1 -1
  46. package/dist/types/modes/theme/theme.d.ts +2 -1
  47. package/dist/types/modes/turn-budget.d.ts +18 -0
  48. package/dist/types/modes/types.d.ts +7 -3
  49. package/dist/types/modes/ultrathink.d.ts +7 -2
  50. package/dist/types/modes/workflow.d.ts +15 -0
  51. package/dist/types/sdk.d.ts +15 -4
  52. package/dist/types/session/agent-session.d.ts +59 -23
  53. package/dist/types/session/session-manager.d.ts +18 -0
  54. package/dist/types/session/session-storage.d.ts +6 -0
  55. package/dist/types/session/shake-types.d.ts +24 -0
  56. package/dist/types/task/executor.d.ts +2 -2
  57. package/dist/types/thinking.d.ts +39 -1
  58. package/dist/types/tiny/device.d.ts +3 -3
  59. package/dist/types/tiny/models.d.ts +34 -1
  60. package/dist/types/tiny/title-protocol.d.ts +4 -0
  61. package/dist/types/tools/index.d.ts +19 -3
  62. package/dist/types/tools/memory-edit.d.ts +1 -1
  63. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  64. package/package.json +10 -10
  65. package/src/auto-thinking/classifier.ts +180 -0
  66. package/src/autoresearch/tools/run-experiment.ts +45 -113
  67. package/src/cli/args.ts +39 -16
  68. package/src/cli/extension-flags.ts +48 -0
  69. package/src/cli/plugin-cli.ts +11 -2
  70. package/src/config/config-file.ts +98 -13
  71. package/src/config/file-lock.ts +60 -17
  72. package/src/config/keybindings.ts +78 -27
  73. package/src/config/model-registry.ts +7 -1
  74. package/src/config/settings-schema.ts +118 -71
  75. package/src/config/settings.ts +12 -0
  76. package/src/edit/hashline/diff.ts +87 -22
  77. package/src/edit/renderer.ts +16 -12
  78. package/src/edit/streaming.ts +17 -6
  79. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  80. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  81. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  82. package/src/eval/__tests__/shared-executors.test.ts +53 -0
  83. package/src/eval/agent-bridge.ts +295 -0
  84. package/src/eval/backend.ts +17 -2
  85. package/src/eval/budget-bridge.ts +48 -0
  86. package/src/eval/idle-timeout.ts +80 -0
  87. package/src/eval/js/executor.ts +35 -7
  88. package/src/eval/js/index.ts +2 -1
  89. package/src/eval/js/shared/local-module-loader.ts +75 -10
  90. package/src/eval/js/shared/prelude.txt +85 -1
  91. package/src/eval/js/tool-bridge.ts +9 -0
  92. package/src/eval/py/executor.ts +41 -14
  93. package/src/eval/py/index.ts +2 -1
  94. package/src/eval/py/prelude.py +132 -1
  95. package/src/exec/bash-executor.ts +2 -3
  96. package/src/extensibility/custom-tools/types.ts +2 -2
  97. package/src/extensibility/extensions/runner.ts +12 -2
  98. package/src/extensibility/plugins/git-url.ts +90 -4
  99. package/src/extensibility/plugins/manager.ts +103 -7
  100. package/src/extensibility/shared-events.ts +2 -2
  101. package/src/internal-urls/docs-index.generated.ts +88 -88
  102. package/src/main.ts +50 -56
  103. package/src/memory-backend/index.ts +1 -1
  104. package/src/memory-backend/resolve.ts +3 -3
  105. package/src/memory-backend/types.ts +3 -3
  106. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  107. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  108. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  109. package/src/modes/acp/acp-agent.ts +13 -3
  110. package/src/modes/components/agent-dashboard.ts +6 -6
  111. package/src/modes/components/custom-editor.ts +4 -11
  112. package/src/modes/components/extensions/state-manager.ts +3 -4
  113. package/src/modes/components/footer.ts +18 -12
  114. package/src/modes/components/hook-selector.ts +86 -20
  115. package/src/modes/components/model-selector.ts +20 -11
  116. package/src/modes/components/oauth-selector.ts +93 -21
  117. package/src/modes/components/omfg-panel.ts +141 -0
  118. package/src/modes/components/settings-defs.ts +9 -2
  119. package/src/modes/components/settings-selector.ts +4 -1
  120. package/src/modes/components/status-line/segments.ts +13 -5
  121. package/src/modes/components/tips.txt +2 -1
  122. package/src/modes/components/tool-execution.ts +38 -19
  123. package/src/modes/components/tree-selector.ts +4 -3
  124. package/src/modes/components/user-message-selector.ts +94 -19
  125. package/src/modes/components/user-message.ts +8 -1
  126. package/src/modes/controllers/command-controller.ts +57 -0
  127. package/src/modes/controllers/event-controller.ts +65 -3
  128. package/src/modes/controllers/input-controller.ts +14 -11
  129. package/src/modes/controllers/omfg-controller.ts +283 -0
  130. package/src/modes/controllers/omfg-rule.ts +647 -0
  131. package/src/modes/controllers/selector-controller.ts +21 -6
  132. package/src/modes/gradient-highlight.ts +23 -6
  133. package/src/modes/interactive-mode.ts +41 -7
  134. package/src/modes/magic-keywords.ts +20 -0
  135. package/src/modes/markdown-prose.ts +247 -0
  136. package/src/modes/orchestrate.ts +17 -11
  137. package/src/modes/shared.ts +3 -11
  138. package/src/modes/theme/theme.ts +6 -0
  139. package/src/modes/turn-budget.ts +31 -0
  140. package/src/modes/types.ts +7 -1
  141. package/src/modes/ultrathink.ts +16 -10
  142. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  143. package/src/modes/workflow.ts +42 -0
  144. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  145. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  146. package/src/prompts/system/omfg-user.md +51 -0
  147. package/src/prompts/system/system-prompt.md +1 -0
  148. package/src/prompts/system/workflow-notice.md +70 -0
  149. package/src/prompts/tools/eval.md +13 -1
  150. package/src/prompts/tools/memory-edit.md +1 -1
  151. package/src/sdk.ts +86 -38
  152. package/src/session/agent-session.ts +558 -80
  153. package/src/session/session-manager.ts +32 -0
  154. package/src/session/session-storage.ts +68 -8
  155. package/src/session/shake-types.ts +44 -0
  156. package/src/slash-commands/builtin-registry.ts +41 -16
  157. package/src/task/executor.ts +3 -3
  158. package/src/task/index.ts +6 -6
  159. package/src/thinking.ts +73 -1
  160. package/src/tiny/device.ts +4 -10
  161. package/src/tiny/models.ts +54 -2
  162. package/src/tiny/title-protocol.ts +11 -1
  163. package/src/tiny/worker.ts +19 -7
  164. package/src/tools/eval.ts +202 -26
  165. package/src/tools/grouped-file-output.ts +9 -2
  166. package/src/tools/index.ts +17 -5
  167. package/src/tools/memory-edit.ts +4 -4
  168. package/src/tools/memory-recall.ts +5 -5
  169. package/src/tools/memory-reflect.ts +5 -5
  170. package/src/tools/memory-retain.ts +4 -4
  171. package/src/tools/render-utils.ts +2 -1
  172. package/src/tools/search.ts +480 -76
  173. package/dist/types/mnemosyne/backend.d.ts +0 -4
  174. package/dist/types/mnemosyne/config.d.ts +0 -29
  175. package/dist/types/mnemosyne/state.d.ts +0 -72
  176. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  177. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
- import { getThinkingLevelMetadata } from "../thinking";
3
+ import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
4
4
  import {
5
5
  TINY_MODEL_DEVICE_DEFAULT,
6
6
  TINY_MODEL_DEVICE_SETTING_OPTIONS,
@@ -12,8 +12,14 @@ import {
12
12
  TINY_MODEL_DTYPE_SETTING_VALUES,
13
13
  } from "../tiny/dtype";
14
14
  import {
15
+ AUTO_THINKING_MODEL_OPTIONS,
16
+ AUTO_THINKING_MODEL_VALUES,
17
+ DEFAULT_SHAKE_SUMMARY_MODEL_KEY,
18
+ ONLINE_AUTO_THINKING_MODEL_KEY,
15
19
  ONLINE_MEMORY_MODEL_KEY,
16
20
  ONLINE_TINY_TITLE_MODEL_KEY,
21
+ SHAKE_SUMMARY_MODEL_OPTIONS,
22
+ SHAKE_SUMMARY_MODEL_VALUES,
17
23
  TINY_MEMORY_MODEL_OPTIONS,
18
24
  TINY_MEMORY_MODEL_VALUES,
19
25
  TINY_TITLE_MODEL_OPTIONS,
@@ -671,13 +677,16 @@ export const SETTINGS_SCHEMA = {
671
677
  // Reasoning and prompts
672
678
  defaultThinkingLevel: {
673
679
  type: "enum",
674
- values: THINKING_EFFORTS,
680
+ values: [...THINKING_EFFORTS, AUTO_THINKING],
675
681
  default: "high",
676
682
  ui: {
677
683
  tab: "model",
678
684
  label: "Thinking Level",
679
685
  description: "Reasoning depth for thinking-capable models",
680
- options: [...THINKING_EFFORTS.map(getThinkingLevelMetadata)],
686
+ options: [
687
+ getConfiguredThinkingLevelMetadata(AUTO_THINKING),
688
+ ...THINKING_EFFORTS.map(getThinkingLevelMetadata),
689
+ ],
681
690
  },
682
691
  },
683
692
 
@@ -1130,12 +1139,13 @@ export const SETTINGS_SCHEMA = {
1130
1139
 
1131
1140
  "compaction.strategy": {
1132
1141
  type: "enum",
1133
- values: ["context-full", "handoff", "off"] as const,
1142
+ values: ["context-full", "handoff", "shake", "shake-summary", "off"] as const,
1134
1143
  default: "context-full",
1135
1144
  ui: {
1136
1145
  tab: "context",
1137
1146
  label: "Compaction Strategy",
1138
- description: "Choose in-place context-full maintenance, auto-handoff, or disable auto maintenance (off)",
1147
+ description:
1148
+ "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), shake with local-model summaries, or disable auto maintenance (off)",
1139
1149
  options: [
1140
1150
  {
1141
1151
  value: "context-full",
@@ -1143,6 +1153,16 @@ export const SETTINGS_SCHEMA = {
1143
1153
  description: "Summarize in-place and keep the current session",
1144
1154
  },
1145
1155
  { value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
1156
+ {
1157
+ value: "shake",
1158
+ label: "Shake",
1159
+ description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
1160
+ },
1161
+ {
1162
+ value: "shake-summary",
1163
+ label: "Shake (summary)",
1164
+ description: "Shake, but compress heavy regions with a local on-device model instead of dropping",
1165
+ },
1146
1166
  {
1147
1167
  value: "off",
1148
1168
  label: "Off",
@@ -1321,70 +1341,70 @@ export const SETTINGS_SCHEMA = {
1321
1341
  "memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
1322
1342
 
1323
1343
  // Memory backend selector — picks between local memories pipeline,
1324
- // Mnemosyne local SQLite, Hindsight remote memory, or off. Legacy
1344
+ // Mnemopi local SQLite, Hindsight remote memory, or off. Legacy
1325
1345
  // `memories.enabled` keeps gating the local backend; see config/settings.ts
1326
1346
  // migration for details.
1327
1347
  "memory.backend": {
1328
1348
  type: "enum",
1329
- values: ["off", "local", "hindsight", "mnemosyne"] as const,
1349
+ values: ["off", "local", "hindsight", "mnemopi"] as const,
1330
1350
  default: "off",
1331
1351
  ui: {
1332
1352
  tab: "memory",
1333
1353
  label: "Memory Backend",
1334
- description: "Off, local summary pipeline, Mnemosyne SQLite, or Hindsight remote memory",
1354
+ description: "Off, local summary pipeline, Mnemopi SQLite, or Hindsight remote memory",
1335
1355
  options: [
1336
1356
  { value: "off", label: "Off", description: "No memory subsystem runs" },
1337
1357
  { value: "local", label: "Local", description: "Local rollout summarisation pipeline (memory_summary.md)" },
1338
1358
  { value: "hindsight", label: "Hindsight", description: "Vectorize Hindsight remote memory service" },
1339
1359
  {
1340
- value: "mnemosyne",
1341
- label: "Mnemosyne",
1360
+ value: "mnemopi",
1361
+ label: "Mnemopi",
1342
1362
  description: "Local SQLite recall/retain backend with optional embeddings",
1343
1363
  },
1344
1364
  ],
1345
1365
  },
1346
1366
  },
1347
1367
 
1348
- // Mnemosyne local SQLite memory backend.
1349
- "mnemosyne.dbPath": {
1368
+ // Mnemopi local SQLite memory backend.
1369
+ "mnemopi.dbPath": {
1350
1370
  type: "string",
1351
1371
  default: undefined,
1352
1372
  ui: {
1353
1373
  tab: "memory",
1354
- label: "Mnemosyne DB Path",
1374
+ label: "Mnemopi DB Path",
1355
1375
  description: "Optional SQLite DB path. Defaults to the agent memories directory.",
1356
- condition: "mnemosyneActive",
1376
+ condition: "mnemopiActive",
1357
1377
  },
1358
1378
  },
1359
- "mnemosyne.bank": {
1379
+ "mnemopi.bank": {
1360
1380
  type: "string",
1361
1381
  default: undefined,
1362
1382
  ui: {
1363
1383
  tab: "memory",
1364
- label: "Mnemosyne Bank",
1384
+ label: "Mnemopi Bank",
1365
1385
  description: "Optional shared bank base name. Per-project modes derive project-local banks from it.",
1366
- condition: "mnemosyneActive",
1386
+ condition: "mnemopiActive",
1367
1387
  },
1368
1388
  },
1369
- "mnemosyne.scoping": {
1389
+ "mnemopi.scoping": {
1370
1390
  type: "enum",
1371
1391
  values: ["global", "per-project", "per-project-tagged"] as const,
1372
1392
  default: "per-project",
1373
1393
  ui: {
1374
1394
  tab: "memory",
1375
- label: "Mnemosyne Scoping",
1395
+ label: "Mnemopi Scoping",
1376
1396
  description:
1377
1397
  "global = one shared bank; per-project = isolated bank per cwd; per-project-tagged = project-local writes plus global recall visibility",
1378
1398
  options: [
1379
1399
  {
1380
1400
  value: "global",
1381
1401
  label: "Global",
1382
- description: "One shared Mnemosyne bank for every project",
1402
+ description: "One shared Mnemopi bank for every project",
1383
1403
  },
1384
1404
  {
1385
1405
  value: "per-project",
1386
1406
  label: "Per project",
1387
- description: "Project-local Mnemosyne bank per cwd basename",
1407
+ description: "Project-local Mnemopi bank per cwd basename",
1388
1408
  },
1389
1409
  {
1390
1410
  value: "per-project-tagged",
@@ -1392,121 +1412,121 @@ export const SETTINGS_SCHEMA = {
1392
1412
  description: "Write to a project-local bank but merge project + shared recall results",
1393
1413
  },
1394
1414
  ],
1395
- condition: "mnemosyneActive",
1415
+ condition: "mnemopiActive",
1396
1416
  },
1397
1417
  },
1398
- "mnemosyne.autoRecall": {
1418
+ "mnemopi.autoRecall": {
1399
1419
  type: "boolean",
1400
1420
  default: true,
1401
1421
  ui: {
1402
1422
  tab: "memory",
1403
- label: "Mnemosyne Auto Recall",
1423
+ label: "Mnemopi Auto Recall",
1404
1424
  description: "Recall local memories into the first turn of each session",
1405
- condition: "mnemosyneActive",
1425
+ condition: "mnemopiActive",
1406
1426
  },
1407
1427
  },
1408
- "mnemosyne.autoRetain": {
1428
+ "mnemopi.autoRetain": {
1409
1429
  type: "boolean",
1410
1430
  default: true,
1411
1431
  ui: {
1412
1432
  tab: "memory",
1413
- label: "Mnemosyne Auto Retain",
1414
- description: "Retain completed conversation turns into local Mnemosyne memory",
1415
- condition: "mnemosyneActive",
1433
+ label: "Mnemopi Auto Retain",
1434
+ description: "Retain completed conversation turns into local Mnemopi memory",
1435
+ condition: "mnemopiActive",
1416
1436
  },
1417
1437
  },
1418
- "mnemosyne.noEmbeddings": {
1438
+ "mnemopi.noEmbeddings": {
1419
1439
  type: "boolean",
1420
1440
  default: false,
1421
1441
  ui: {
1422
1442
  tab: "memory",
1423
- label: "Mnemosyne Disable Embeddings",
1443
+ label: "Mnemopi Disable Embeddings",
1424
1444
  description: "Force deterministic FTS-only recall instead of vector embeddings",
1425
- condition: "mnemosyneActive",
1445
+ condition: "mnemopiActive",
1426
1446
  },
1427
1447
  },
1428
- "mnemosyne.embeddingModel": {
1448
+ "mnemopi.embeddingModel": {
1429
1449
  type: "string",
1430
1450
  default: undefined,
1431
1451
  ui: {
1432
1452
  tab: "memory",
1433
- label: "Mnemosyne Embedding Model",
1434
- description: "Optional embedding model override passed to Mnemosyne",
1435
- condition: "mnemosyneActive",
1453
+ label: "Mnemopi Embedding Model",
1454
+ description: "Optional embedding model override passed to Mnemopi",
1455
+ condition: "mnemopiActive",
1436
1456
  },
1437
1457
  },
1438
- "mnemosyne.embeddingApiUrl": {
1458
+ "mnemopi.embeddingApiUrl": {
1439
1459
  type: "string",
1440
1460
  default: undefined,
1441
1461
  ui: {
1442
1462
  tab: "memory",
1443
- label: "Mnemosyne Embedding API URL",
1444
- description: "Optional OpenAI-compatible embedding endpoint passed to Mnemosyne",
1445
- condition: "mnemosyneActive",
1463
+ label: "Mnemopi Embedding API URL",
1464
+ description: "Optional OpenAI-compatible embedding endpoint passed to Mnemopi",
1465
+ condition: "mnemopiActive",
1446
1466
  },
1447
1467
  },
1448
- "mnemosyne.embeddingApiKey": {
1468
+ "mnemopi.embeddingApiKey": {
1449
1469
  type: "string",
1450
1470
  default: undefined,
1451
1471
  ui: {
1452
1472
  tab: "memory",
1453
- label: "Mnemosyne Embedding API Key",
1454
- description: "Optional embedding API key passed to Mnemosyne",
1455
- condition: "mnemosyneActive",
1473
+ label: "Mnemopi Embedding API Key",
1474
+ description: "Optional embedding API key passed to Mnemopi",
1475
+ condition: "mnemopiActive",
1456
1476
  },
1457
1477
  },
1458
- "mnemosyne.llmMode": {
1478
+ "mnemopi.llmMode": {
1459
1479
  type: "enum",
1460
1480
  values: ["none", "smol", "remote"] as const,
1461
1481
  default: "smol",
1462
1482
  ui: {
1463
1483
  tab: "memory",
1464
- label: "Mnemosyne LLM Mode",
1484
+ label: "Mnemopi LLM Mode",
1465
1485
  description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
1466
- condition: "mnemosyneActive",
1486
+ condition: "mnemopiActive",
1467
1487
  options: [
1468
- { value: "none", label: "None", description: "Disable Mnemosyne LLM-backed extraction" },
1488
+ { value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
1469
1489
  { value: "smol", label: "Smol", description: "Use the configured pi-ai smol model" },
1470
- { value: "remote", label: "Remote", description: "Use the Mnemosyne remote LLM settings below" },
1490
+ { value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
1471
1491
  ],
1472
1492
  },
1473
1493
  },
1474
- "mnemosyne.llmBaseUrl": {
1494
+ "mnemopi.llmBaseUrl": {
1475
1495
  type: "string",
1476
1496
  default: undefined,
1477
1497
  ui: {
1478
1498
  tab: "memory",
1479
- label: "Mnemosyne LLM Base URL",
1480
- description: "Optional OpenAI-compatible LLM endpoint for Mnemosyne remote mode",
1481
- condition: "mnemosyneActive",
1499
+ label: "Mnemopi LLM Base URL",
1500
+ description: "Optional OpenAI-compatible LLM endpoint for Mnemopi remote mode",
1501
+ condition: "mnemopiActive",
1482
1502
  },
1483
1503
  },
1484
- "mnemosyne.llmApiKey": {
1504
+ "mnemopi.llmApiKey": {
1485
1505
  type: "string",
1486
1506
  default: undefined,
1487
1507
  ui: {
1488
1508
  tab: "memory",
1489
- label: "Mnemosyne LLM API Key",
1490
- description: "Optional LLM API key for Mnemosyne remote mode",
1491
- condition: "mnemosyneActive",
1509
+ label: "Mnemopi LLM API Key",
1510
+ description: "Optional LLM API key for Mnemopi remote mode",
1511
+ condition: "mnemopiActive",
1492
1512
  },
1493
1513
  },
1494
- "mnemosyne.llmModel": {
1514
+ "mnemopi.llmModel": {
1495
1515
  type: "string",
1496
1516
  default: undefined,
1497
1517
  ui: {
1498
1518
  tab: "memory",
1499
- label: "Mnemosyne LLM Model",
1500
- description: "Optional LLM model name for Mnemosyne remote mode",
1501
- condition: "mnemosyneActive",
1519
+ label: "Mnemopi LLM Model",
1520
+ description: "Optional LLM model name for Mnemopi remote mode",
1521
+ condition: "mnemopiActive",
1502
1522
  },
1503
1523
  },
1504
- "mnemosyne.retainEveryNTurns": { type: "number", default: 4 },
1505
- "mnemosyne.recallLimit": { type: "number", default: 8 },
1506
- "mnemosyne.recallContextTurns": { type: "number", default: 3 },
1507
- "mnemosyne.recallMaxQueryChars": { type: "number", default: 4000 },
1508
- "mnemosyne.injectionTokenLimit": { type: "number", default: 5000 },
1509
- "mnemosyne.debug": { type: "boolean", default: false },
1524
+ "mnemopi.retainEveryNTurns": { type: "number", default: 4 },
1525
+ "mnemopi.recallLimit": { type: "number", default: 8 },
1526
+ "mnemopi.recallContextTurns": { type: "number", default: 3 },
1527
+ "mnemopi.recallMaxQueryChars": { type: "number", default: 4000 },
1528
+ "mnemopi.injectionTokenLimit": { type: "number", default: 5000 },
1529
+ "mnemopi.debug": { type: "boolean", default: false },
1510
1530
 
1511
1531
  // Hindsight (https://hindsight.vectorize.io)
1512
1532
  "hindsight.apiUrl": {
@@ -2954,7 +2974,7 @@ export const SETTINGS_SCHEMA = {
2954
2974
  tab: "providers",
2955
2975
  label: "Tiny Model Device",
2956
2976
  description:
2957
- "ONNX execution provider for local tiny models (titles + memory). Default picks DirectML on Windows, CUDA on Linux x64, CPU elsewhere. The PI_TINY_DEVICE env var overrides this.",
2977
+ "ONNX execution provider for local tiny models (titles + memory). Default uses CPU-only inference. The PI_TINY_DEVICE env var overrides this.",
2958
2978
  options: TINY_MODEL_DEVICE_SETTING_OPTIONS,
2959
2979
  },
2960
2980
  },
@@ -2978,12 +2998,39 @@ export const SETTINGS_SCHEMA = {
2978
2998
  tab: "memory",
2979
2999
  label: "Memory Model",
2980
3000
  description:
2981
- "Mnemosyne LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
2982
- condition: "mnemosyneActive",
3001
+ "Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
3002
+ condition: "mnemopiActive",
2983
3003
  options: TINY_MEMORY_MODEL_OPTIONS,
2984
3004
  },
2985
3005
  },
2986
3006
 
3007
+ "providers.autoThinkingModel": {
3008
+ type: "enum",
3009
+ values: AUTO_THINKING_MODEL_VALUES,
3010
+ default: ONLINE_AUTO_THINKING_MODEL_KEY,
3011
+ ui: {
3012
+ tab: "model",
3013
+ label: "Auto Thinking Model",
3014
+ description:
3015
+ "Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model",
3016
+ condition: "autoThinkingActive",
3017
+ options: AUTO_THINKING_MODEL_OPTIONS,
3018
+ },
3019
+ },
3020
+
3021
+ "providers.shakeSummaryModel": {
3022
+ type: "enum",
3023
+ values: SHAKE_SUMMARY_MODEL_VALUES,
3024
+ default: DEFAULT_SHAKE_SUMMARY_MODEL_KEY,
3025
+ ui: {
3026
+ tab: "context",
3027
+ label: "Shake Summary Model",
3028
+ description:
3029
+ "Local on-device model used by /shake summary and the shake-summary compaction strategy to compress heavy regions. Runs entirely on-device; downloads on first use. Falls back to plain elide when unavailable.",
3030
+ options: SHAKE_SUMMARY_MODEL_OPTIONS,
3031
+ },
3032
+ },
3033
+
2987
3034
  "providers.kimiApiFormat": {
2988
3035
  type: "enum",
2989
3036
  values: ["openai", "anthropic"] as const,
@@ -3272,7 +3319,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
3272
3319
 
3273
3320
  export interface CompactionSettings {
3274
3321
  enabled: boolean;
3275
- strategy: "context-full" | "handoff" | "off";
3322
+ strategy: "context-full" | "handoff" | "shake" | "shake-summary" | "off";
3276
3323
  thresholdPercent: number;
3277
3324
  thresholdTokens: number;
3278
3325
  reserveTokens: number;
@@ -692,6 +692,18 @@ export class Settings {
692
692
  raw.memory = memoryRoot;
693
693
  }
694
694
 
695
+ // Rename the legacy local `mnemosyne` memory backend to `mnemopi`.
696
+ // - `memory.backend: "mnemosyne"` now selects the renamed backend.
697
+ // - the top-level `mnemosyne` settings object becomes `mnemopi`.
698
+ // Idempotent: skips the object move once `mnemopi` is materialised.
699
+ if (memoryBackendObj && memoryBackendObj.backend === "mnemosyne") {
700
+ memoryBackendObj.backend = "mnemopi";
701
+ }
702
+ if ("mnemosyne" in raw && !("mnemopi" in raw)) {
703
+ raw.mnemopi = raw.mnemosyne;
704
+ delete raw.mnemosyne;
705
+ }
706
+
695
707
  // hindsight: dynamicBankId/agentName -> scoping enum + bankId
696
708
  // - dynamicBankId=true → scoping="per-project" (closest semantic match;
697
709
  // the legacy `agent::project::channel::user` tuple was per-project in
@@ -5,17 +5,25 @@
5
5
  * pair to {@link generateDiffString} so the renderer can show the diff
6
6
  * while the tool call is still streaming.
7
7
  *
8
- * Validation is intentionally light: only the section snapshot tag is checked
9
- * (so the preview goes red when anchors are stale), no plan-mode guards
10
- * and no auto-generated-file refusal those belong on the write path.
8
+ * Uses the same snapshot-tag semantics as the apply path: a live content-hash
9
+ * match is accepted even when the tag was minted by a source that did not keep
10
+ * history, and stale tags recover through the session snapshot store when possible.
11
11
  */
12
12
  import {
13
+ type ApplyResult,
14
+ applyEdits,
15
+ computeFileHash,
16
+ type Edit,
13
17
  Patch as HashlinePatch,
18
+ hasBlockEdit,
19
+ MismatchError,
14
20
  missingSnapshotTagMessage,
15
21
  normalizeToLF,
16
22
  type Patch,
17
23
  type PatchSection,
18
- type Snapshot,
24
+ parsePatchStreaming,
25
+ Recovery,
26
+ resolveBlockEdits,
19
27
  type SnapshotStore,
20
28
  stripBom,
21
29
  } from "@oh-my-pi/hashline";
@@ -31,6 +39,12 @@ export interface HashlineDiffOptions {
31
39
  * preview path only.
32
40
  */
33
41
  streaming?: boolean;
42
+ /**
43
+ * Skip snapshot-tag validation. Streaming previews use this so transient
44
+ * stale/missing tags do not flash re-read errors while the model is still
45
+ * authoring input; the final apply path still validates through Patcher.
46
+ */
47
+ skipHashValidation?: boolean;
34
48
  }
35
49
 
36
50
  async function readSectionText(absolutePath: string, sectionPath: string): Promise<string> {
@@ -42,24 +56,79 @@ async function readSectionText(absolutePath: string, sectionPath: string): Promi
42
56
  }
43
57
  }
44
58
 
45
- function snapshotMatchesCurrent(snapshot: Snapshot, currentText: string): boolean {
46
- return snapshot.text === currentText;
59
+ function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
60
+ return edits.some(edit => {
61
+ if (edit.kind === "delete") return true;
62
+ if (edit.kind === "block") return true;
63
+ return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
64
+ });
47
65
  }
48
- function validateSectionHash(
66
+
67
+ function createMismatchError(
49
68
  section: PatchSection,
50
69
  absolutePath: string,
51
- text: string,
70
+ normalized: string,
52
71
  snapshots: SnapshotStore,
53
- ): string | null {
54
- if (section.fileHash === undefined) {
55
- // The snapshot tag is mandatory on every section — head/tail inserts
56
- // included — to keep this preview path in lockstep with the apply path
57
- // (`Patcher.prepare`), which rejects tagless sections unconditionally.
58
- return missingSnapshotTagMessage(section.path);
72
+ expected: string,
73
+ ): MismatchError {
74
+ return new MismatchError({
75
+ path: section.path,
76
+ expectedFileHash: expected,
77
+ actualFileHash: computeFileHash(normalized),
78
+ fileLines: normalized.split("\n"),
79
+ anchorLines: section.collectAnchorLines(),
80
+ hashRecognized: snapshots.byHash(absolutePath, expected) !== null,
81
+ });
82
+ }
83
+
84
+ function parsePreviewEdits(section: PatchSection, streaming: boolean | undefined): readonly Edit[] {
85
+ return streaming ? parsePatchStreaming(section.diff).edits : section.edits;
86
+ }
87
+
88
+ function resolvePreviewEdits(args: {
89
+ section: PatchSection;
90
+ absolutePath: string;
91
+ normalized: string;
92
+ snapshots: SnapshotStore;
93
+ expected: string | undefined;
94
+ liveMatches: boolean;
95
+ edits: readonly Edit[];
96
+ }): readonly Edit[] {
97
+ const { section, absolutePath, normalized, snapshots, expected, liveMatches, edits } = args;
98
+ if (!hasBlockEdit(edits)) return edits;
99
+ const baseText = expected === undefined || liveMatches ? normalized : snapshots.byHash(absolutePath, expected)?.text;
100
+ if (baseText === undefined) {
101
+ throw createMismatchError(section, absolutePath, normalized, snapshots, expected ?? "");
59
102
  }
60
- const snapshot = snapshots.byHash(absolutePath, section.fileHash);
61
- if (snapshot && snapshotMatchesCurrent(snapshot, text)) return null;
62
- return `Hashline snapshot tag mismatch for ${section.path}: section is bound to #${section.fileHash}, but current file does not match that snapshot; re-read and try again.`;
103
+ return resolveBlockEdits(edits, baseText, section.path, nativeBlockResolver, { onUnresolved: "throw" });
104
+ }
105
+
106
+ function applyPreviewEdits(args: {
107
+ section: PatchSection;
108
+ absolutePath: string;
109
+ normalized: string;
110
+ snapshots: SnapshotStore;
111
+ options: HashlineDiffOptions;
112
+ }): ApplyResult {
113
+ const { section, absolutePath, normalized, snapshots, options } = args;
114
+ const expected = section.fileHash;
115
+ if (!options.skipHashValidation && expected === undefined) {
116
+ throw new Error(missingSnapshotTagMessage(section.path));
117
+ }
118
+ const liveMatches = expected !== undefined && computeFileHash(normalized) === expected;
119
+ const edits = parsePreviewEdits(section, options.streaming);
120
+ const resolved = resolvePreviewEdits({ section, absolutePath, normalized, snapshots, expected, liveMatches, edits });
121
+ if (options.skipHashValidation || expected === undefined || liveMatches) return applyEdits(normalized, resolved);
122
+ if (!hasAnchorScopedEdit(resolved)) return applyEdits(normalized, resolved);
123
+
124
+ const recovered = new Recovery(snapshots).tryRecover({
125
+ path: absolutePath,
126
+ currentText: normalized,
127
+ fileHash: expected,
128
+ edits: resolved,
129
+ });
130
+ if (recovered) return recovered;
131
+ throw createMismatchError(section, absolutePath, normalized, snapshots, expected);
63
132
  }
64
133
 
65
134
  export async function computeHashlineSectionDiff(
@@ -73,11 +142,7 @@ export async function computeHashlineSectionDiff(
73
142
  const rawContent = await readSectionText(absolutePath, section.path);
74
143
  const { text: content } = stripBom(rawContent);
75
144
  const normalized = normalizeToLF(content);
76
- const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
77
- if (hashError) return { error: hashError };
78
- const result = options.streaming
79
- ? section.applyPartialTo(normalized, nativeBlockResolver)
80
- : section.applyTo(normalized, nativeBlockResolver);
145
+ const result = applyPreviewEdits({ section, absolutePath, normalized, snapshots, options });
81
146
  if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
82
147
  return generateDiffString(normalized, result.text);
83
148
  } catch (err) {
@@ -235,23 +235,27 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
235
235
 
236
236
  function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
237
237
  if (!diff) return "";
238
- // Hunk-aware truncation keeps the change rows themselves visible. Tail-mode
239
- // pins the visible window to the bottom of the diff so newly streamed
240
- // hunks stay on screen as more arrives, instead of leaving the user stuck
241
- // staring at the head of the file while the tail scrolls offscreen.
242
- const {
243
- text: truncatedDiff,
244
- hiddenHunks,
245
- hiddenLines,
246
- } = truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, EDIT_STREAMING_PREVIEW_LINES, { fromTail: true });
238
+ // "Cursor" tail window: pin the last EDIT_STREAMING_PREVIEW_LINES rows to the
239
+ // bottom of the diff so freshly streamed changes stay on screen, and accept
240
+ // the trailing rows "from the back" once the diff outgrows the window. The
241
+ // whole-file diff is recomputed on every streamed chunk and its Myers
242
+ // alignment is not monotonic in payload length, so a hunk-aware window that
243
+ // kept whole change segments gained and lost rows tick to tick — the box
244
+ // stuttered, and the earlier high-water fix traded that for a half-empty
245
+ // rectangle. A strict fixed-height window keeps the box steady and always
246
+ // full of real diff context instead of blank padding.
247
+ const allLines = diff.replace(/\n+$/u, "").split("\n");
248
+ const hiddenLines = Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
249
+ const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
247
250
  let text = "\n\n";
248
- if (hiddenHunks > 0 || hiddenLines > 0) {
251
+ if (hiddenLines > 0) {
252
+ const hiddenHunks = getDiffStats(allLines.slice(0, hiddenLines).join("\n")).hunks;
249
253
  const remainder: string[] = [];
250
254
  if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
251
- if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
255
+ remainder.push(`${hiddenLines} more lines`);
252
256
  text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
253
257
  }
254
- text += renderDiffColored(truncatedDiff, { filePath: rawPath });
258
+ text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
255
259
  text += uiTheme.fg("dim", `\n(${label})`);
256
260
  return text;
257
261
  }
@@ -314,8 +314,14 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
314
314
  },
315
315
  async computeDiffPreview(args, ctx) {
316
316
  if (typeof args.input !== "string" || args.input.length === 0) return null;
317
- const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
318
- if (input.length === 0) return null;
317
+ // Unlike apply_patch, hashline previews flow through `applyPartialTo`,
318
+ // whose streaming-tolerant parser (`parsePatchStreaming` `endStreaming`)
319
+ // drops a payload-less trailing op and projects a partially-typed payload
320
+ // line onto the file as it grows. Trimming the trailing partial line here
321
+ // would instead strip the sole payload of a single-op `replace`/`insert`
322
+ // for almost the entire stream, collapsing the preview to "No changes" and
323
+ // rendering a blank box. Feed the raw in-flight text straight through.
324
+ const input = args.input;
319
325
  ctx.signal.throwIfAborted();
320
326
 
321
327
  let sections: readonly HashlineInputSection[];
@@ -347,12 +353,17 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
347
353
  const section = sectionsToProcess[i];
348
354
  const result = await computeHashlineSectionDiff(section, ctx.cwd, ctx.snapshots, {
349
355
  streaming: ctx.isStreaming,
356
+ skipHashValidation: ctx.isStreaming === true,
350
357
  });
351
358
  ctx.signal.throwIfAborted();
352
- // In a multi-section preview, ignore parse/apply errors from the
353
- // last section: it's still streaming and the partial op may not
354
- // parse yet. Earlier sections are stable and stay rendered.
355
- if (sectionsToProcess.length > 1 && i === trailingProcessedIndex && "error" in result) {
359
+ // Ignore parse/apply errors from the trailing (actively-typed)
360
+ // section while streaming: a mid-typed op may transiently resolve to
361
+ // "No changes" or an out-of-bounds anchor, and surfacing that would
362
+ // wipe the already-stable previews (or, for a lone section, the prior
363
+ // good frame). Returning no entry preserves the last preview. Earlier
364
+ // sections, and every section once args are complete, stay rendered so
365
+ // real errors still reach the model.
366
+ if ((ctx.isStreaming || sectionsToProcess.length > 1) && i === trailingProcessedIndex && "error" in result) {
356
367
  continue;
357
368
  }
358
369
  previews.push(toPerFilePreview(section.path, result));