shortcutxl 0.3.81 → 0.3.83

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 (129) hide show
  1. package/BINARY-INVENTORY.json +13 -13
  2. package/CHANGELOG.md +10 -0
  3. package/dist/app/extensions/wrapper.js +3 -3
  4. package/dist/app/modes/action/agent.d.ts +1 -1
  5. package/dist/app/modes/index.js +12 -13
  6. package/dist/app/resources/skills.js +3 -0
  7. package/dist/app/session/session-catalog.d.ts +4 -2
  8. package/dist/app/session/session-catalog.js +15 -89
  9. package/dist/app/session/session-manager.d.ts +2 -0
  10. package/dist/app/session/session-manager.js +4 -0
  11. package/dist/app/session/session-metadata.d.ts +16 -0
  12. package/dist/app/session/session-metadata.js +75 -0
  13. package/dist/app/session/session-reader.d.ts +9 -0
  14. package/dist/app/session/session-reader.js +16 -0
  15. package/dist/app/session/session-store-types.d.ts +2 -0
  16. package/dist/app/subagents/defaults.d.ts +2 -2
  17. package/dist/app/sync/remote-skill-files.d.ts +6 -0
  18. package/dist/app/sync/remote-skill-files.js +24 -0
  19. package/dist/app/sync/skills-download.d.ts +4 -2
  20. package/dist/app/sync/skills-download.js +44 -3
  21. package/dist/app/sync/skills-status.js +14 -4
  22. package/dist/app/sync/skills-upload.d.ts +1 -0
  23. package/dist/app/sync/skills-upload.js +13 -3
  24. package/dist/app/tools/task/index.d.ts +1 -0
  25. package/dist/app/tools/task/task.d.ts +10 -1
  26. package/dist/app/tools/task/task.js +28 -11
  27. package/dist/cli/args.d.ts +2 -0
  28. package/dist/cli/args.js +12 -1
  29. package/dist/cli/runtime-args.d.ts +12 -0
  30. package/dist/cli/runtime-args.js +25 -0
  31. package/dist/cli/runtime-launch-profile.d.ts +21 -0
  32. package/dist/cli/runtime-launch-profile.js +23 -0
  33. package/dist/cli.js +2212 -1897
  34. package/dist/constants.d.ts +4 -0
  35. package/dist/constants.js +11 -0
  36. package/dist/contracts/agent-api.d.ts +43 -2
  37. package/dist/contracts/agent-api.js +50 -0
  38. package/dist/contracts/agent-session-store.d.ts +12 -0
  39. package/dist/contracts/host-tool-request-id.d.ts +3 -0
  40. package/dist/contracts/host-tool-request-id.js +7 -0
  41. package/dist/core/embedded-agent-facade.d.ts +3 -2
  42. package/dist/core/embedded-agent-facade.js +10 -2
  43. package/dist/core/index.d.ts +1 -0
  44. package/dist/core/index.js +1 -0
  45. package/dist/core/pending-host-tool-requests.d.ts +2 -0
  46. package/dist/core/pending-host-tool-requests.js +4 -2
  47. package/dist/core/tool-definition.d.ts +4 -0
  48. package/dist/core/tool-definition.js +6 -0
  49. package/dist/embedded-agent/compose.js +130 -4
  50. package/dist/embedded-agent/context-review/activity.d.ts +4 -0
  51. package/dist/embedded-agent/context-review/activity.js +22 -0
  52. package/dist/embedded-agent/context-review/agent.d.ts +14 -0
  53. package/dist/embedded-agent/context-review/agent.js +90 -0
  54. package/dist/embedded-agent/context-review/result.d.ts +7 -0
  55. package/dist/embedded-agent/context-review/result.js +17 -0
  56. package/dist/embedded-agent/host-runtime-options.d.ts +2 -1
  57. package/dist/embedded-agent/host-runtime-options.js +80 -4
  58. package/dist/embedded-agent/host-tools/build-tool-list.d.ts +5 -3
  59. package/dist/embedded-agent/host-tools/build-tool-list.js +10 -3
  60. package/dist/embedded-agent/host-tools/get-tool-info/shared.d.ts +1 -1
  61. package/dist/embedded-agent/host-tools/mode-host-tools.d.ts +1 -0
  62. package/dist/embedded-agent/host-tools/mode-host-tools.js +1 -0
  63. package/dist/embedded-agent/host-tools/read-skill/contract.d.ts +12 -5
  64. package/dist/embedded-agent/host-tools/read-skill/contract.js +28 -15
  65. package/dist/embedded-agent/host-tools/task/contract.d.ts +2 -0
  66. package/dist/embedded-agent/host-tools/task/contract.js +27 -12
  67. package/dist/embedded-agent/index.d.ts +9 -1
  68. package/dist/embedded-agent/worker-bridge/dispatch.js +13 -4
  69. package/dist/embedded-agent/worker-bridge/host-client.d.ts +2 -1
  70. package/dist/embedded-agent/worker-bridge/host-client.js +25 -9
  71. package/dist/launcher/native-app.d.ts +18 -0
  72. package/dist/launcher/native-app.js +17 -0
  73. package/dist/main-helpers.d.ts +2 -1
  74. package/dist/main-helpers.js +4 -4
  75. package/dist/main.js +27 -52
  76. package/dist/mode-names.d.ts +48 -0
  77. package/dist/mode-names.js +31 -0
  78. package/dist/remote-control/session-registry.js +2 -2
  79. package/dist/rpc/index.d.ts +1 -1
  80. package/dist/rpc/rpc-client.js +7 -1
  81. package/dist/rpc/rpc-host.d.ts +4 -0
  82. package/dist/rpc/rpc-host.js +55 -5
  83. package/dist/rpc/rpc-mode.d.ts +1 -1
  84. package/dist/rpc/rpc-types.d.ts +21 -2
  85. package/dist/shell/components/selectors/session-selector.js +1 -1
  86. package/dist/shell/interactive/interactive-mode-options.d.ts +1 -0
  87. package/dist/shell/interactive/skills-workflow.d.ts +2 -0
  88. package/dist/shell/interactive/skills-workflow.js +26 -7
  89. package/dist/shell/status/update-reminder.d.ts +1 -0
  90. package/dist/shell/status/update-reminder.js +2 -1
  91. package/dist/startup/interactive-commands.js +2 -0
  92. package/dist/startup/runtime-routing.d.ts +2 -1
  93. package/dist/startup/runtime-routing.js +5 -20
  94. package/dist/startup/terminal-update-flow.d.ts +20 -0
  95. package/dist/startup/terminal-update-flow.js +13 -0
  96. package/dist/startup/update-execution.d.ts +11 -0
  97. package/dist/startup/update-execution.js +30 -0
  98. package/dist/startup/update-prompt.js +2 -1
  99. package/dist/subagent-model-policy.d.ts +11 -1
  100. package/dist/subagent-model-policy.js +15 -1
  101. package/dist/update.d.ts +1 -0
  102. package/dist/update.js +1 -0
  103. package/native-app/main/chunks/{linux-zbDx-phY.js → linux-BNzCbzEv.js} +6 -6
  104. package/native-app/main/chunks/{mac-B0tPNjg3.js → mac-Brys445r.js} +6 -6
  105. package/native-app/main/chunks/{setup-worker-protocol-CD16E33n.js → setup-worker-protocol-DkeuNPNE.js} +135 -135
  106. package/native-app/main/chunks/{shared-Bfla4vPs.js → shared-DdhTDmUY.js} +1 -1
  107. package/native-app/main/index.js +903 -210
  108. package/native-app/main/setup-worker.js +5 -5
  109. package/native-app/package.json +1 -1
  110. package/native-app/preload/index.cjs +38 -19
  111. package/native-app/renderer/assets/{index-C6Qju_hO.css → index-Beh8sTpE.css} +241 -122
  112. package/native-app/renderer/assets/{index-CcP3xE7V.js → index-hkPksres.js} +1624 -762
  113. package/native-app/renderer/index.html +2 -2
  114. package/package.json +31 -1
  115. package/plugins/shortcutxl/SKILL.md +4 -2
  116. package/plugins/shortcutxl/skills/shortcutxl/SKILL.md +4 -2
  117. package/xll/ShortcutXL.xll +0 -0
  118. package/xll/python/Lib/site-packages/httpx-0.28.1.dist-info/RECORD +1 -1
  119. package/xll/python/Lib/site-packages/idna-3.18.dist-info/RECORD +1 -1
  120. package/xll/python/Lib/site-packages/pip-26.2.dist-info/RECORD +3 -3
  121. package/xll/python/Lib/site-packages/pywin32-311.dist-info/RECORD +2 -2
  122. package/xll/python/Scripts/httpx.exe +0 -0
  123. package/xll/python/Scripts/idna.exe +0 -0
  124. package/xll/python/Scripts/pip.exe +0 -0
  125. package/xll/python/Scripts/pip3.13.exe +0 -0
  126. package/xll/python/Scripts/pip3.exe +0 -0
  127. package/xll/python/Scripts/pywin32_postinstall.exe +0 -0
  128. package/xll/python/Scripts/pywin32_testall.exe +0 -0
  129. package/native-app/main/update-helper.js +0 -92
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-12T04:56:55.420Z",
3
+ "generatedAt": "2026-08-14T08:00:44.785Z",
4
4
  "package": "shortcutxl",
5
5
  "build": {
6
6
  "builder": "github-actions",
7
7
  "repository": "lyfegame/shortcut",
8
- "sourceRef": "refs/heads/codex/native-resources-and-clipboard",
9
- "sourceCommit": "ff226182eb12799fcc6c1a9b8ede4fbfb18bfa7e",
10
- "workflowRef": "lyfegame/shortcut/.github/workflows/release-shortcutxl.yml@refs/heads/codex/native-resources-and-clipboard",
11
- "runId": "31564834411",
8
+ "sourceRef": "refs/heads/fix/native-tab-lifecycle-e2e",
9
+ "sourceCommit": "05b4e8d19e60e7761e4839f723e34f10aec5732b",
10
+ "workflowRef": "lyfegame/shortcut/.github/workflows/release-shortcutxl.yml@refs/heads/fix/native-tab-lifecycle-e2e",
11
+ "runId": "31782090568",
12
12
  "runnerOs": "Windows",
13
13
  "runnerImage": "win25-vs2026",
14
14
  "runnerImageVersion": "20260803.193.1"
@@ -22,7 +22,7 @@
22
22
  "files": [
23
23
  {
24
24
  "path": "xll/ShortcutXL.xll",
25
- "sha256": "f5bffdeda390326397a23c83648c8de66b3d58a51d1af7e2ea936fed18ec754c",
25
+ "sha256": "184bd94bf10a566d242761aed8036e3db819447dcf2f65c94b39a55d91985e68",
26
26
  "source": "ShortcutXL native XLL build",
27
27
  "version": "package",
28
28
  "builtBy": "shortcut",
@@ -534,7 +534,7 @@
534
534
  },
535
535
  {
536
536
  "path": "xll/python/Scripts/httpx.exe",
537
- "sha256": "0e2294a43a498d892ab8e5191e5a301aebc3f19879f23ea257fff5f52a1f7b32",
537
+ "sha256": "65250768d249e621c8d772506fb01447e82fe9ee4f886e7652d20c76257df743",
538
538
  "source": "httpx console launcher installed into embedded Python",
539
539
  "version": "see packaged httpx distribution",
540
540
  "builtBy": "third-party",
@@ -542,7 +542,7 @@
542
542
  },
543
543
  {
544
544
  "path": "xll/python/Scripts/idna.exe",
545
- "sha256": "502b2aeaa0c5b515150137bc35ec134d12f5df43b4b4af262e5115f030994ce7",
545
+ "sha256": "b62b33859e93f2f5e3aec1d90810fe89f3798c4b60e52e40587b134f87268241",
546
546
  "source": "Python package console launcher installed into embedded Python",
547
547
  "version": "see owning Python package metadata in site-packages",
548
548
  "builtBy": "third-party",
@@ -550,7 +550,7 @@
550
550
  },
551
551
  {
552
552
  "path": "xll/python/Scripts/pip.exe",
553
- "sha256": "5b29e4cd0dea0c8698301ff87d6c9c228cfcdf5ca56635e3c4e3d22bdcd1c21d",
553
+ "sha256": "cdf622bf555bb3f7d192501f331010f7ef29dd70a13dcec2df8e3e160cb5626e",
554
554
  "source": "pip console launcher installed into embedded Python",
555
555
  "version": "see packaged pip distribution",
556
556
  "builtBy": "third-party",
@@ -558,7 +558,7 @@
558
558
  },
559
559
  {
560
560
  "path": "xll/python/Scripts/pip3.13.exe",
561
- "sha256": "5b29e4cd0dea0c8698301ff87d6c9c228cfcdf5ca56635e3c4e3d22bdcd1c21d",
561
+ "sha256": "cdf622bf555bb3f7d192501f331010f7ef29dd70a13dcec2df8e3e160cb5626e",
562
562
  "source": "pip console launcher installed into embedded Python",
563
563
  "version": "see packaged pip distribution",
564
564
  "builtBy": "third-party",
@@ -566,7 +566,7 @@
566
566
  },
567
567
  {
568
568
  "path": "xll/python/Scripts/pip3.exe",
569
- "sha256": "5b29e4cd0dea0c8698301ff87d6c9c228cfcdf5ca56635e3c4e3d22bdcd1c21d",
569
+ "sha256": "cdf622bf555bb3f7d192501f331010f7ef29dd70a13dcec2df8e3e160cb5626e",
570
570
  "source": "pip console launcher installed into embedded Python",
571
571
  "version": "see packaged pip distribution",
572
572
  "builtBy": "third-party",
@@ -574,7 +574,7 @@
574
574
  },
575
575
  {
576
576
  "path": "xll/python/Scripts/pywin32_postinstall.exe",
577
- "sha256": "7cd7d1f3296f6fcf00e56f758b363100a519be237729070b67c544d3d2948f59",
577
+ "sha256": "05901aedb7c65ac48a31dede95ccc8a7daf8cef8cf923af364635310c57f2c45",
578
578
  "source": "Python package console launcher installed into embedded Python",
579
579
  "version": "see owning Python package metadata in site-packages",
580
580
  "builtBy": "third-party",
@@ -582,7 +582,7 @@
582
582
  },
583
583
  {
584
584
  "path": "xll/python/Scripts/pywin32_testall.exe",
585
- "sha256": "34fbc20da3daac9f50f77884dabb243ec47e301dcd3d7b40e5fe483bc9e1a9d6",
585
+ "sha256": "af7a61cb9722914d056923434850b756fda5cc388f6c9d3b47586efac880848c",
586
586
  "source": "Python package console launcher installed into embedded Python",
587
587
  "version": "see owning Python package metadata in site-packages",
588
588
  "builtBy": "third-party",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.83]
4
+
5
+ - **More reliable native app chats**
6
+
7
+ ## [0.3.82]
8
+
9
+ - **Safer CLI and app updates**
10
+ - **More reliable native chats for app**
11
+ - **Drag and drop file attachments for app**
12
+
3
13
  ## [0.3.81]
4
14
 
5
15
  - **Smoother native setup and upgrades** - `shortcut app` now guides first-run prerequisites and required Excel integration updates directly in the app.
@@ -4,14 +4,14 @@
4
4
  * Wraps ToolDefinitions with tool_call / tool_result event hooks so
5
5
  * extensions can block or modify tool execution.
6
6
  */
7
+ import { cloneToolDefinition } from '../../core/tool-definition.js';
7
8
  /**
8
9
  * Wrap a tool with extension callbacks for interception.
9
10
  * - Emits tool_call event before execution (can block)
10
11
  * - Emits tool_result event after execution (can modify result)
11
12
  */
12
13
  export function wrapToolWithExtensions(tool, runner) {
13
- return {
14
- ...tool,
14
+ return cloneToolDefinition(tool, {
15
15
  execute: async (toolCallId, params, signal, onUpdate, ctx) => {
16
16
  // Emit tool_call event - extensions can block execution
17
17
  if (runner.hasHandlers('tool_call')) {
@@ -73,7 +73,7 @@ export function wrapToolWithExtensions(tool, runner) {
73
73
  throw err;
74
74
  }
75
75
  }
76
- };
76
+ });
77
77
  }
78
78
  /**
79
79
  * Wrap all tools with extension callbacks.
@@ -5,7 +5,7 @@
5
5
  * Can switch to plan or installation mode.
6
6
  */
7
7
  import type { AgentDefinition } from '../../agent-definition.js';
8
- export declare const ACTION_AGENT_TOOL_NAMES: ("write" | "execute_code" | "execute_tool" | "get_tool_info" | "mcp" | "refresh_context" | "send_message" | "take_screenshot" | "task" | "bash" | "switch_from_action_mode")[];
8
+ export declare const ACTION_AGENT_TOOL_NAMES: ("bash" | "write" | "execute_code" | "execute_tool" | "get_tool_info" | "mcp" | "refresh_context" | "send_message" | "take_screenshot" | "task" | "switch_from_action_mode")[];
9
9
  /** Construct the action-mode agent definition with the full execution toolset. */
10
10
  export declare function actionAgent(): AgentDefinition;
11
11
  //# sourceMappingURL=agent.d.ts.map
@@ -10,7 +10,7 @@ export { installationAgent } from './installation/agent.js';
10
10
  export { manageAgent } from './manage/agent.js';
11
11
  export { planAgent } from './plan/agent.js';
12
12
  export { reviewAgent } from './review/agent.js';
13
- import { MODE } from '../../mode-names.js';
13
+ import { isAgentMode, MODE } from '../../mode-names.js';
14
14
  import { actionAgent } from './action/agent.js';
15
15
  import { askAgent } from './ask/agent.js';
16
16
  import { installationAgent } from './installation/agent.js';
@@ -18,25 +18,24 @@ import { manageAgent } from './manage/agent.js';
18
18
  import { MODE_METADATA } from './metadata.js';
19
19
  import { planAgent } from './plan/agent.js';
20
20
  import { reviewAgent } from './review/agent.js';
21
- const MODE_FACTORIES = new Map([
22
- [MODE.INSTALLATION, installationAgent],
23
- [MODE.ACTION, actionAgent],
24
- [MODE.PLAN, planAgent],
25
- [MODE.ASK, askAgent],
26
- [MODE.MANAGE, manageAgent],
27
- [MODE.REVIEW, reviewAgent]
28
- ]);
21
+ const MODE_FACTORIES = {
22
+ [MODE.INSTALLATION]: installationAgent,
23
+ [MODE.ACTION]: actionAgent,
24
+ [MODE.PLAN]: planAgent,
25
+ [MODE.ASK]: askAgent,
26
+ [MODE.MANAGE]: manageAgent,
27
+ [MODE.REVIEW]: reviewAgent
28
+ };
29
29
  export function getModes() {
30
30
  return MODE_METADATA.map((metadata) => {
31
- const factory = MODE_FACTORIES.get(metadata.name);
32
- if (!factory) {
31
+ if (!isAgentMode(metadata.name)) {
33
32
  throw new Error(`No mode factory registered for "${metadata.name}".`);
34
33
  }
35
- return factory();
34
+ return MODE_FACTORIES[metadata.name]();
36
35
  });
37
36
  }
38
37
  export function getMode(name) {
39
- return MODE_FACTORIES.get(name)?.();
38
+ return isAgentMode(name) ? MODE_FACTORIES[name]() : undefined;
40
39
  }
41
40
  export function getModeNames() {
42
41
  return MODE_METADATA.map((mode) => mode.name);
@@ -200,7 +200,10 @@ export function loadSkillsFromDir(options) {
200
200
  const { dir, source } = options;
201
201
  const skills = [];
202
202
  const diagnostics = [];
203
+ const archivedUserSkillsPrefix = `${resolve(dir, '_archive')}${sep}`;
203
204
  for (const filePath of discoverSkillFiles(dir)) {
205
+ if (source === 'user' && resolve(filePath).startsWith(archivedUserSkillsPrefix))
206
+ continue;
204
207
  const result = loadSkillFromFile(filePath, source);
205
208
  if (result.skill) {
206
209
  skills.push(result.skill);
@@ -11,8 +11,10 @@ export interface SessionInfo {
11
11
  created: Date;
12
12
  modified: Date;
13
13
  messageCount: number;
14
- firstMessage: string;
15
- lastMessage: string;
14
+ /** First user message text, absent when the session has no readable messages. */
15
+ firstMessage?: string;
16
+ /** Most recent user message text, absent when the session has no readable messages. */
17
+ lastMessage?: string;
16
18
  allMessagesText: string;
17
19
  }
18
20
  export declare function buildSessionInfo(filePath: string): Promise<SessionInfo | null>;
@@ -3,53 +3,8 @@ import { existsSync } from 'fs';
3
3
  import { readdir, readFile, stat } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { getSessionsDir } from '../../config.js';
6
+ import { analyzeSessionEntries } from './session-metadata.js';
6
7
  import { getDefaultSessionDir } from './session-paths.js';
7
- function isMessageWithContent(message) {
8
- return typeof message.role === 'string' && 'content' in message;
9
- }
10
- function extractTextContent(message) {
11
- const content = message.content;
12
- if (typeof content === 'string') {
13
- return content;
14
- }
15
- return content
16
- .filter((block) => block.type === 'text')
17
- .map((block) => block.text)
18
- .join(' ');
19
- }
20
- function getLastActivityTime(entries) {
21
- let lastActivityTime;
22
- for (const entry of entries) {
23
- if (entry.type !== 'message')
24
- continue;
25
- const message = entry.message;
26
- if (!isMessageWithContent(message))
27
- continue;
28
- if (message.role !== 'user' && message.role !== 'assistant')
29
- continue;
30
- const msgTimestamp = message.timestamp;
31
- if (typeof msgTimestamp === 'number') {
32
- lastActivityTime = Math.max(lastActivityTime ?? 0, msgTimestamp);
33
- continue;
34
- }
35
- const entryTimestamp = entry.timestamp;
36
- if (typeof entryTimestamp === 'string') {
37
- const t = new Date(entryTimestamp).getTime();
38
- if (!Number.isNaN(t)) {
39
- lastActivityTime = Math.max(lastActivityTime ?? 0, t);
40
- }
41
- }
42
- }
43
- return lastActivityTime;
44
- }
45
- function getSessionModifiedDate(entries, header, statsMtime) {
46
- const lastActivityTime = getLastActivityTime(entries);
47
- if (typeof lastActivityTime === 'number' && lastActivityTime > 0) {
48
- return new Date(lastActivityTime);
49
- }
50
- const headerTime = typeof header.timestamp === 'string' ? new Date(header.timestamp).getTime() : NaN;
51
- return !Number.isNaN(headerTime) ? new Date(headerTime) : statsMtime;
52
- }
53
8
  export async function buildSessionInfo(filePath) {
54
9
  try {
55
10
  const content = await readFile(filePath, 'utf8');
@@ -71,51 +26,22 @@ export async function buildSessionInfo(filePath) {
71
26
  if (header.type !== 'session')
72
27
  return null;
73
28
  const stats = await stat(filePath);
74
- let messageCount = 0;
75
- let firstMessage = '';
76
- let lastMessage = '';
77
- const allMessages = [];
78
- let name;
79
- for (const entry of entries) {
80
- if (entry.type === 'session_info') {
81
- const infoEntry = entry;
82
- if (infoEntry.name) {
83
- name = infoEntry.name.trim();
84
- }
85
- }
86
- if (entry.type !== 'message')
87
- continue;
88
- messageCount++;
89
- const message = entry.message;
90
- if (!isMessageWithContent(message))
91
- continue;
92
- if (message.role !== 'user' && message.role !== 'assistant')
93
- continue;
94
- const textContent = extractTextContent(message);
95
- if (!textContent)
96
- continue;
97
- allMessages.push(textContent);
98
- if (!firstMessage && message.role === 'user') {
99
- firstMessage = textContent;
100
- }
101
- if (message.role === 'user') {
102
- lastMessage = textContent;
103
- }
104
- }
105
- const sessionHeader = header;
106
- const modified = getSessionModifiedDate(entries, sessionHeader, stats.mtime);
29
+ const metadata = analyzeSessionEntries(entries, {
30
+ fallbackTimestamp: stats.mtime,
31
+ searchText: true
32
+ });
107
33
  return {
108
34
  path: filePath,
109
- id: sessionHeader.id,
110
- cwd: typeof sessionHeader.cwd === 'string' ? sessionHeader.cwd : '',
111
- name,
112
- parentSessionPath: sessionHeader.parentSession,
113
- created: new Date(sessionHeader.timestamp),
114
- modified,
115
- messageCount,
116
- firstMessage: firstMessage || '(no messages)',
117
- lastMessage: lastMessage || firstMessage || '(no messages)',
118
- allMessagesText: allMessages.join(' ')
35
+ id: header.id,
36
+ cwd: typeof header.cwd === 'string' ? header.cwd : '',
37
+ name: metadata.name,
38
+ parentSessionPath: header.parentSession,
39
+ created: new Date(metadata.createdAt),
40
+ modified: new Date(metadata.modifiedAt),
41
+ messageCount: metadata.messageCount,
42
+ firstMessage: metadata.firstMessage,
43
+ lastMessage: metadata.lastMessage,
44
+ allMessagesText: metadata.allMessagesText ?? ''
119
45
  };
120
46
  }
121
47
  catch {
@@ -1,6 +1,7 @@
1
1
  import type { ImageContent, Message, TextContent } from '../../ai/types.js';
2
2
  import { type NewSessionOptions, type SessionContext, type SessionEntry, type SessionHeader, type SessionTreeNode } from '../../core/session-schema.js';
3
3
  import { type CustomMessage } from '../messages.js';
4
+ import { type SessionMetadata } from './session-metadata.js';
4
5
  import type { SessionPersistenceBackend } from './session-persistence-backend.js';
5
6
  /**
6
7
  * Manages conversation sessions as append-only trees stored in JSONL files.
@@ -36,6 +37,7 @@ export declare class SessionManager {
36
37
  getSessionDir(): string;
37
38
  getSessionId(): string;
38
39
  getSessionFile(): string | undefined;
40
+ getSessionMetadata(): SessionMetadata;
39
41
  _persist(entry: SessionEntry): void;
40
42
  private _appendEntry;
41
43
  private _reloadPersistedArchive;
@@ -1,6 +1,7 @@
1
1
  /** Owns app-session lifecycle, persistence, and turn execution outside the browser embedded worker. */
2
2
  import { randomUUID } from 'crypto';
3
3
  import { buildSessionContext, CURRENT_SESSION_VERSION } from '../../core/session-schema.js';
4
+ import { analyzeSessionEntries } from './session-metadata.js';
4
5
  import { migrateSessionEntries } from './session-schema-node-io.js';
5
6
  /** Generate a unique short ID (8 hex chars, collision-checked) */
6
7
  function generateId(byId) {
@@ -164,6 +165,9 @@ export class SessionManager {
164
165
  getSessionFile() {
165
166
  return this.sessionFile;
166
167
  }
168
+ getSessionMetadata() {
169
+ return analyzeSessionEntries(this.fileEntries);
170
+ }
167
171
  _persist(entry) {
168
172
  if (!this.persist || !this.sessionFile || !this.persistenceBackend)
169
173
  return;
@@ -0,0 +1,16 @@
1
+ import type { RuntimeSessionMetadata } from '../../contracts/agent-session-store.js';
2
+ import type { FileEntry } from '../../core/session-schema.js';
3
+ export type SessionMetadata = RuntimeSessionMetadata;
4
+ export interface SessionMetadataOptions {
5
+ /** Used when the session header carries no usable timestamp. */
6
+ fallbackTimestamp?: Date;
7
+ /**
8
+ * Also concatenate every message into `allMessagesText` for full-text search.
9
+ * Off by default: it copies the entire transcript, and snapshots are published
10
+ * often enough that doing it unconditionally is pure waste.
11
+ */
12
+ searchText?: boolean;
13
+ }
14
+ /** Derives stable display and ordering metadata from the canonical session entries. */
15
+ export declare function analyzeSessionEntries(entries: readonly FileEntry[], { fallbackTimestamp, searchText }?: SessionMetadataOptions): SessionMetadata;
16
+ //# sourceMappingURL=session-metadata.d.ts.map
@@ -0,0 +1,75 @@
1
+ function isMessageWithContent(message) {
2
+ return (typeof message === 'object' &&
3
+ message !== null &&
4
+ typeof message.role === 'string' &&
5
+ 'content' in message);
6
+ }
7
+ function extractTextContent(message) {
8
+ const content = message.content;
9
+ if (typeof content === 'string')
10
+ return content;
11
+ return content
12
+ .filter((block) => block.type === 'text')
13
+ .map((block) => block.text)
14
+ .join(' ');
15
+ }
16
+ function validIsoTimestamp(value) {
17
+ if (typeof value !== 'string' || Number.isNaN(Date.parse(value)))
18
+ return undefined;
19
+ return new Date(value).toISOString();
20
+ }
21
+ /** Derives stable display and ordering metadata from the canonical session entries. */
22
+ export function analyzeSessionEntries(entries, { fallbackTimestamp = new Date(0), searchText = false } = {}) {
23
+ const header = entries.find((entry) => entry.type === 'session');
24
+ const fallbackIso = fallbackTimestamp.toISOString();
25
+ const createdAt = validIsoTimestamp(header?.timestamp) ?? fallbackIso;
26
+ let modifiedAtMs = Date.parse(createdAt);
27
+ let messageCount = 0;
28
+ let firstMessage;
29
+ let lastMessage;
30
+ let name;
31
+ const allMessages = [];
32
+ for (const entry of entries) {
33
+ if (entry.type === 'session_info') {
34
+ const nextName = entry.name?.trim();
35
+ if (nextName)
36
+ name = nextName;
37
+ }
38
+ if (entry.type !== 'message')
39
+ continue;
40
+ messageCount += 1;
41
+ const message = entry.message;
42
+ if (!isMessageWithContent(message))
43
+ continue;
44
+ if (message.role !== 'user' && message.role !== 'assistant')
45
+ continue;
46
+ const messageTimestamp = message.timestamp;
47
+ const entryTimestamp = validIsoTimestamp(entry.timestamp);
48
+ const activityTime = typeof messageTimestamp === 'number' && Number.isFinite(messageTimestamp)
49
+ ? messageTimestamp
50
+ : entryTimestamp
51
+ ? Date.parse(entryTimestamp)
52
+ : undefined;
53
+ if (activityTime !== undefined)
54
+ modifiedAtMs = Math.max(modifiedAtMs, activityTime);
55
+ const text = extractTextContent(message).trim();
56
+ if (!text)
57
+ continue;
58
+ if (searchText)
59
+ allMessages.push(text);
60
+ if (!firstMessage && message.role === 'user')
61
+ firstMessage = text;
62
+ if (message.role === 'user')
63
+ lastMessage = text;
64
+ }
65
+ return {
66
+ createdAt,
67
+ modifiedAt: new Date(modifiedAtMs).toISOString(),
68
+ name,
69
+ firstMessage,
70
+ lastMessage: lastMessage ?? firstMessage,
71
+ ...(searchText ? { allMessagesText: allMessages.join(' ') } : {}),
72
+ messageCount
73
+ };
74
+ }
75
+ //# sourceMappingURL=session-metadata.js.map
@@ -0,0 +1,9 @@
1
+ /** Read-only helpers for host UIs that need saved chats without starting an agent runtime. */
2
+ import type { AgentMessage } from '../../core/core-types.js';
3
+ import { type SessionInfo } from './session-catalog.js';
4
+ export interface PersistedSessionPreview {
5
+ session: SessionInfo;
6
+ messages: AgentMessage[];
7
+ }
8
+ export declare function readPersistedSession(sessionPath: string): Promise<PersistedSessionPreview | null>;
9
+ //# sourceMappingURL=session-reader.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { buildSessionContext } from '../../core/session-schema.js';
2
+ import { buildSessionInfo } from './session-catalog.js';
3
+ import { loadEntriesFromFile, migrateSessionEntries } from './session-schema-node-io.js';
4
+ export async function readPersistedSession(sessionPath) {
5
+ const session = await buildSessionInfo(sessionPath);
6
+ if (!session)
7
+ return null;
8
+ const entries = loadEntriesFromFile(session.path);
9
+ if (entries.length === 0 && session.messageCount > 0) {
10
+ throw new Error('Session could not be read');
11
+ }
12
+ migrateSessionEntries(entries);
13
+ const messages = buildSessionContext(entries.filter((entry) => entry.type !== 'session')).messages;
14
+ return { session, messages };
15
+ }
16
+ //# sourceMappingURL=session-reader.js.map
@@ -1,6 +1,7 @@
1
1
  import type { ImageContent, TextContent } from '../../ai/types.js';
2
2
  import type { AgentMessage } from '../../core/index.js';
3
3
  import type { NewSessionOptions, SessionContext, SessionEntry, SessionHeader, SessionTreeNode } from '../../core/session-schema.js';
4
+ import type { SessionMetadata } from './session-metadata.js';
4
5
  export interface SessionInfoStore {
5
6
  getSessionName(): string | undefined;
6
7
  appendSessionInfo(name: string): string;
@@ -18,6 +19,7 @@ export interface ProductSessionStore extends SessionInfoStore {
18
19
  getSessionDir(): string;
19
20
  getSessionFile(): string | undefined;
20
21
  getSessionId(): string;
22
+ getSessionMetadata(): SessionMetadata;
21
23
  getTree(): SessionTreeNode[];
22
24
  newSession(options?: NewSessionOptions): string | undefined;
23
25
  setSessionFile(sessionPath: string): void;
@@ -1,6 +1,6 @@
1
- import { type SubagentSelectableModel } from '../../subagent-model-policy.js';
1
+ import { type SubagentSelectableModelRef } from '../../subagent-model-policy.js';
2
2
  export declare const DEFAULT_SUBAGENT_TIMEOUT_SECONDS: number;
3
- export type TaskModelChoice = SubagentSelectableModel['ref'];
3
+ export type TaskModelChoice = SubagentSelectableModelRef;
4
4
  export type TaskModelSelection = {
5
5
  readonly provider: string;
6
6
  readonly modelId: string;
@@ -1,5 +1,6 @@
1
1
  import { type StorageContext, type StorageEntry, type StorageOwnerOptions } from './storage-client.js';
2
2
  export declare const SKILLS_STORAGE_PREFIX = ".shortcut/skills/";
3
+ export declare const PERSONAL_SKILL_ARCHIVE_DIR = "_archive";
3
4
  export interface RemoteSkillFile {
4
5
  entry: StorageEntry;
5
6
  storagePath: string;
@@ -14,6 +15,11 @@ export interface DuplicateTeamSkill {
14
15
  skillName: string;
15
16
  ownerTeamIds: string[];
16
17
  }
18
+ /** Active cloud copies win when the same skill name also has archived history. */
19
+ export declare function partitionPersonalSkillFiles(files: readonly RemoteSkillFile[]): {
20
+ activeFiles: RemoteSkillFile[];
21
+ archiveOnlySkillNames: Set<string>;
22
+ };
17
23
  /** List all skill files for a given storage context. */
18
24
  export declare function listSkillFiles(accessToken: string, storageContext: StorageContext, ownerTeamId?: string): Promise<RemoteSkillFile[]>;
19
25
  export declare function listAuthorizedTeamSkillFiles(accessToken: string, authorizedSkillTeamIds: readonly string[]): Promise<TeamSkillFilesResult>;
@@ -1,5 +1,29 @@
1
1
  import { storageList } from './storage-client.js';
2
2
  export const SKILLS_STORAGE_PREFIX = '.shortcut/skills/';
3
+ export const PERSONAL_SKILL_ARCHIVE_DIR = '_archive';
4
+ const ARCHIVED_SKILL_PREFIX = `${PERSONAL_SKILL_ARCHIVE_DIR}/`;
5
+ /** Active cloud copies win when the same skill name also has archived history. */
6
+ export function partitionPersonalSkillFiles(files) {
7
+ const activeFiles = [];
8
+ const activeSkillNames = new Set();
9
+ const archivedSkillNames = new Set();
10
+ for (const file of files) {
11
+ if (!file.entry.path.startsWith(ARCHIVED_SKILL_PREFIX)) {
12
+ activeFiles.push(file);
13
+ const skillName = file.entry.path.split('/')[0];
14
+ if (skillName)
15
+ activeSkillNames.add(skillName);
16
+ continue;
17
+ }
18
+ const skillName = file.entry.path.slice(ARCHIVED_SKILL_PREFIX.length).split('/')[0];
19
+ if (skillName)
20
+ archivedSkillNames.add(skillName);
21
+ }
22
+ return {
23
+ activeFiles,
24
+ archiveOnlySkillNames: new Set([...archivedSkillNames].filter((skillName) => !activeSkillNames.has(skillName)))
25
+ };
26
+ }
3
27
  /** List all skill files for a given storage context. */
4
28
  export async function listSkillFiles(accessToken, storageContext, ownerTeamId) {
5
29
  const options = ownerTeamId ? { teamId: ownerTeamId } : {};
@@ -8,7 +8,7 @@ import { type StorageContext } from './storage-client.js';
8
8
  export interface SkillDownloadApprovalRequest {
9
9
  skillName: string;
10
10
  source: StorageContext;
11
- kind: 'create' | 'update';
11
+ kind: 'archive' | 'create' | 'update';
12
12
  modifiedFiles: number;
13
13
  localOnlyFiles: number;
14
14
  remoteOnlyFiles: number;
@@ -23,7 +23,7 @@ export interface SkillFileDiff {
23
23
  export interface SkillDownloadChange {
24
24
  skillName: string;
25
25
  source: StorageContext;
26
- kind: 'create' | 'update';
26
+ kind: 'archive' | 'create' | 'update';
27
27
  localModifiedAt?: string;
28
28
  cloudModifiedAt?: string;
29
29
  fileDiffs: SkillFileDiff[];
@@ -31,6 +31,8 @@ export interface SkillDownloadChange {
31
31
  export interface SkillsDownloadResult {
32
32
  /** Skill names that had at least one file written */
33
33
  downloaded: string[];
34
+ /** Local active copies removed after the cloud skill was archived. */
35
+ removed: string[];
34
36
  /** Skill names where local and cloud copies were already identical */
35
37
  skipped: string[];
36
38
  /** Skill names skipped after the user rejected overwrite */