@oar-sh/oar 0.9.9 → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/README.md +78 -9
- package/bin/bat/oar.bat +46 -46
- package/package.json +2 -2
- package/server/README.md +286 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +35 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +36 -0
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/public/app/agent-exchange-fold-view.mjs +248 -0
- package/server/public/app/api-client.js +61 -0
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +186 -35
- package/server/public/app/chat-actions-menu-state.mjs +116 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/conversation-view.js +361 -85
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/host-suspend-ui.js +6 -3
- package/server/public/app/journal-view.js +146 -26
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +3 -0
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/settings-modal.js +53 -1
- package/server/public/app/socket-handlers.js +59 -28
- package/server/public/app/store.js +19 -2
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +534 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/thinking-follow.js +4 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
- package/server/public/app/transcript-jump-bottom.mjs +99 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +396 -5
- package/server/repositories/message-repository.mjs +29 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/messages-routes.mjs +515 -50
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +72 -0
- package/server/server-runtime.mjs +245 -62
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/preview-proxy-server.mjs +41 -23
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1935 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +494 -2
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/session-activity-tracker.mjs +487 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +157 -5
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The header's ⋯ menu: one decision for its trigger and for every entry, with
|
|
2
|
+
// or without a conversation open. No DOM here; bootstrap.js applies the result.
|
|
3
|
+
//
|
|
4
|
+
// The menu holds two kinds of entries. Relay-wide ones (Settings, Restart web
|
|
5
|
+
// relay, Select Models, Check Usage, the queue, Day/Night, the PWA version)
|
|
6
|
+
// are shown whenever the app is signed in, so Settings can be reached on a
|
|
7
|
+
// fresh install, where no conversation exists yet. Per-conversation ones (Edit
|
|
8
|
+
// title, Share, Archive, Inspect tmux, Change CWD, Git changes, Kill session,
|
|
9
|
+
// Open on claude.ai) are hidden, not greyed, while nothing is open: an empty
|
|
10
|
+
// screen offers what can be done there and nothing else.
|
|
11
|
+
//
|
|
12
|
+
// The shared read-only view is its own branch: Day/Night only, whatever the
|
|
13
|
+
// conversation state, and the trigger stays usable even when the shared
|
|
14
|
+
// conversation failed to load.
|
|
15
|
+
|
|
16
|
+
export const CHAT_ACTIONS_MENU_TRIGGER_ID = 'chat-actions-menu-btn';
|
|
17
|
+
|
|
18
|
+
/** Entries that are text, not actions: `hidden` is applied, nothing else. */
|
|
19
|
+
export const CHAT_ACTIONS_MENU_META_IDS = Object.freeze([
|
|
20
|
+
'chat-menu-pwa-version',
|
|
21
|
+
'chat-menu-queue-status',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** Every entry the resolver decides, in menu order. */
|
|
25
|
+
export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
26
|
+
'chat-menu-pwa-version',
|
|
27
|
+
'chat-menu-queue-status',
|
|
28
|
+
'chat-menu-edit-title',
|
|
29
|
+
'chat-menu-shared-theme-toggle',
|
|
30
|
+
'chat-menu-usage',
|
|
31
|
+
'chat-menu-compact',
|
|
32
|
+
'chat-menu-share-conversation',
|
|
33
|
+
'chat-menu-archive',
|
|
34
|
+
'chat-menu-refresh-history',
|
|
35
|
+
'chat-menu-inspect-tmux',
|
|
36
|
+
'chat-menu-select-models',
|
|
37
|
+
'chat-menu-change-cwd',
|
|
38
|
+
'chat-menu-git-changes',
|
|
39
|
+
'chat-menu-open-cloud-session',
|
|
40
|
+
'chat-menu-settings',
|
|
41
|
+
'chat-menu-restart-relay',
|
|
42
|
+
'chat-menu-suspend-host',
|
|
43
|
+
'chat-menu-kill-session',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Entries that are never shown: retired actions kept in the markup. */
|
|
47
|
+
const ALWAYS_HIDDEN_IDS = new Set(['chat-menu-compact', 'chat-menu-refresh-history']);
|
|
48
|
+
|
|
49
|
+
const shown = (disabled = false) => ({ hidden: false, disabled: !!disabled });
|
|
50
|
+
const hidden = () => ({ hidden: true, disabled: true });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} input
|
|
54
|
+
* @param {boolean} [input.sharedMode] the shared read-only view
|
|
55
|
+
* @param {string|null} [input.conversationId] the open conversation, if any
|
|
56
|
+
* @param {object|null} [input.conversation] its record, if known
|
|
57
|
+
* @param {string|null} [input.sdkSessionId] its bound session, if any
|
|
58
|
+
* @param {boolean} [input.isCloud] it runs on Claude Cloud
|
|
59
|
+
* @param {string|null} [input.cloudSessionUrl] its claude.ai session link, if reported
|
|
60
|
+
* @param {boolean} [input.editing] its title is being edited
|
|
61
|
+
* @param {boolean} [input.suspendVisible] the "show Suspend host" setting
|
|
62
|
+
* @returns {{ trigger: { hidden: boolean, disabled: boolean }, entries: Record<string, { hidden: boolean, disabled: boolean }> }}
|
|
63
|
+
*/
|
|
64
|
+
export function resolveChatActionsMenuState({
|
|
65
|
+
sharedMode = false,
|
|
66
|
+
conversationId = null,
|
|
67
|
+
conversation = null,
|
|
68
|
+
sdkSessionId = null,
|
|
69
|
+
isCloud = false,
|
|
70
|
+
cloudSessionUrl = null,
|
|
71
|
+
editing = false,
|
|
72
|
+
suspendVisible = false,
|
|
73
|
+
} = {}) {
|
|
74
|
+
const entries = {};
|
|
75
|
+
if (sharedMode) {
|
|
76
|
+
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = hidden();
|
|
77
|
+
entries['chat-menu-shared-theme-toggle'] = shown();
|
|
78
|
+
return { trigger: { hidden: false, disabled: false }, entries };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const convId = String(conversationId || '').trim();
|
|
82
|
+
const hasConversation = !!convId;
|
|
83
|
+
const sessionId = String(sdkSessionId || conversation?.sdkSessionId || '').trim();
|
|
84
|
+
const cloud = hasConversation && !!isCloud;
|
|
85
|
+
const sessionUrl = String(cloudSessionUrl || '').trim();
|
|
86
|
+
const perConversation = () => (hasConversation ? shown() : hidden());
|
|
87
|
+
|
|
88
|
+
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = shown();
|
|
89
|
+
for (const id of ALWAYS_HIDDEN_IDS) entries[id] = hidden();
|
|
90
|
+
entries['chat-menu-shared-theme-toggle'] = hidden();
|
|
91
|
+
entries['chat-menu-edit-title'] = perConversation();
|
|
92
|
+
entries['chat-menu-share-conversation'] = perConversation();
|
|
93
|
+
entries['chat-menu-archive'] = perConversation();
|
|
94
|
+
entries['chat-menu-inspect-tmux'] = perConversation();
|
|
95
|
+
entries['chat-menu-git-changes'] = perConversation();
|
|
96
|
+
// A cloud session has no working directory on the relay host to change.
|
|
97
|
+
entries['chat-menu-change-cwd'] = hasConversation && !cloud ? shown() : hidden();
|
|
98
|
+
entries['chat-menu-open-cloud-session'] = cloud && sessionUrl ? shown() : hidden();
|
|
99
|
+
entries['chat-menu-suspend-host'] = suspendVisible ? shown() : hidden();
|
|
100
|
+
// Kill needs a bound session; the entry stays visible so the user sees why
|
|
101
|
+
// it is not available (today's rule).
|
|
102
|
+
entries['chat-menu-kill-session'] = hasConversation ? shown(!sessionId) : hidden();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
// Only a title edit in progress holds the trigger; with nothing open there
|
|
106
|
+
// is no title, so the trigger can never be stuck disabled.
|
|
107
|
+
trigger: { hidden: false, disabled: hasConversation && !!editing },
|
|
108
|
+
entries,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The ids of the entries a state shows, in menu order. */
|
|
113
|
+
export function visibleChatActionsMenuEntryIds(state) {
|
|
114
|
+
const entries = state?.entries || {};
|
|
115
|
+
return CHAT_ACTIONS_MENU_ENTRY_IDS.filter((id) => entries[id] && !entries[id].hidden);
|
|
116
|
+
}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// and the server's `cloud` payload field), so the one sync function below is
|
|
8
8
|
// idempotent and is simply called whenever the header is synced. A
|
|
9
9
|
// conversation of any other provider gets every element back exactly as it was.
|
|
10
|
+
// The ⋯ menu is not touched here: its entries are decided in one place
|
|
11
|
+
// (chat-actions-menu-state.mjs), which reads the cloud state through
|
|
12
|
+
// isCurrentConversationClaudeCloud() and currentCloudSessionUrl().
|
|
10
13
|
|
|
11
14
|
import { conversations, currentConvId, IS_SHARED_VIEW } from './store.js';
|
|
12
15
|
import { openExternalNavigation } from './external-link-policy.mjs';
|
|
@@ -32,21 +35,10 @@ export function isCurrentConversationClaudeCloud() {
|
|
|
32
35
|
return !IS_SHARED_VIEW && isClaudeCloudConversation(currentConversation());
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (hide) {
|
|
40
|
-
if (!element.hidden) {
|
|
41
|
-
element.hidden = true;
|
|
42
|
-
element.dataset.cloudHidden = '1';
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (element.dataset.cloudHidden === '1') {
|
|
47
|
-
element.hidden = false;
|
|
48
|
-
delete element.dataset.cloudHidden;
|
|
49
|
-
}
|
|
38
|
+
/** The claude.ai link of the open cloud conversation, once the worker reported one. */
|
|
39
|
+
export function currentCloudSessionUrl() {
|
|
40
|
+
if (!isCurrentConversationClaudeCloud()) return null;
|
|
41
|
+
return buildCloudLineModel(currentConversation()?.cloud)?.sessionUrl || null;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
export function syncClaudeCloudConversationUi() {
|
|
@@ -65,13 +57,9 @@ export function syncClaudeCloudConversationUi() {
|
|
|
65
57
|
line.hidden = !lineHtml;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
// The
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const openSessionButton = document.getElementById('chat-menu-open-cloud-session');
|
|
72
|
-
if (openSessionButton) {
|
|
73
|
-
openSessionButton.hidden = !(isCloud && buildCloudLineModel(conversation?.cloud)?.sessionUrl);
|
|
74
|
-
}
|
|
60
|
+
// The ⋯ menu entries (no Change CWD for a cloud chat, Open on claude.ai once
|
|
61
|
+
// there is a link) are decided with the rest of the menu in
|
|
62
|
+
// chat-actions-menu-state.mjs, from `isCloud` and currentCloudSessionUrl().
|
|
75
63
|
|
|
76
64
|
const fileInput = document.getElementById('image-input');
|
|
77
65
|
if (fileInput) {
|
|
@@ -22,6 +22,8 @@ export function hasUploadingAttachments(attachments = []) {
|
|
|
22
22
|
// Why a message sent right now queues instead of steering — worker-reported
|
|
23
23
|
// hold reasons mapped to a human title. Stopping a turn lives on the message
|
|
24
24
|
// bubbles, never here, so every state below is a Send/Steer/Queue shape.
|
|
25
|
+
export const SETUP_REQUIRED_TEXT = 'Set up a provider to start chatting';
|
|
26
|
+
|
|
25
27
|
const STEERING_HOLD_TITLES = Object.freeze({
|
|
26
28
|
question: 'Will steer in after you answer the question',
|
|
27
29
|
compaction: 'Will steer in once the conversation finishes compacting',
|
|
@@ -50,7 +52,21 @@ export function deriveComposerControlState({
|
|
|
50
52
|
// An archived conversation: its worker is stopped and it is out of the
|
|
51
53
|
// live list; sending would start it again behind the user's back.
|
|
52
54
|
archived = false,
|
|
55
|
+
// No conversation open and no provider known to be ready (a fresh install):
|
|
56
|
+
// a send here would create a Copilot chat that cannot run. The composer is
|
|
57
|
+
// locked, with a placeholder that says what to do, until readiness flips.
|
|
58
|
+
setupRequired = false,
|
|
53
59
|
} = {}) {
|
|
60
|
+
if (setupRequired) {
|
|
61
|
+
return {
|
|
62
|
+
action: 'send',
|
|
63
|
+
label: 'Send',
|
|
64
|
+
title: SETUP_REQUIRED_TEXT,
|
|
65
|
+
disabled: true,
|
|
66
|
+
composerDisabled: true,
|
|
67
|
+
placeholder: SETUP_REQUIRED_TEXT,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
54
70
|
if (archived) {
|
|
55
71
|
return {
|
|
56
72
|
action: 'send',
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// When the composer may hold the focus without raising a phone's on-screen
|
|
2
|
+
// keyboard, without the DOM. A focused text field shows no keyboard until the
|
|
3
|
+
// user touches the page; the next tap anywhere that keeps the focus there
|
|
4
|
+
// brings it up. So a phone gets no focus at page load, and a tap that only
|
|
5
|
+
// scrolls (the transcript's ⌄) keeps the focus only while the keyboard is
|
|
6
|
+
// already up.
|
|
7
|
+
//
|
|
8
|
+
// Whether the keyboard is up is read from the visual viewport: with it open
|
|
9
|
+
// the visible area is a keyboard's height shorter than the tallest it has been
|
|
10
|
+
// at the same width. Android shrinks the layout with it
|
|
11
|
+
// (interactive-widget=resizes-content), iOS only the visual viewport; both
|
|
12
|
+
// show here. A rotation changes the width, so each width has its own tallest.
|
|
13
|
+
|
|
14
|
+
/** A visible area this much shorter than its tallest has a keyboard under it. */
|
|
15
|
+
export const KEYBOARD_MIN_SHRINK_PX = 100;
|
|
16
|
+
export const KEYBOARD_MIN_SHRINK_RATIO = 0.15;
|
|
17
|
+
|
|
18
|
+
/** The tallest visible height seen at each layout width. */
|
|
19
|
+
export function createViewportBaseline() {
|
|
20
|
+
const tallestByWidth = new Map();
|
|
21
|
+
return {
|
|
22
|
+
note(width, height) {
|
|
23
|
+
const key = Math.round(Number(width) || 0);
|
|
24
|
+
const value = Math.max(0, Number(height) || 0);
|
|
25
|
+
const tallest = Math.max(tallestByWidth.get(key) || 0, value);
|
|
26
|
+
tallestByWidth.set(key, tallest);
|
|
27
|
+
return tallest;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isOnScreenKeyboardOpen({ editableFocused = false, visibleHeight = 0, tallestHeight = 0 } = {}) {
|
|
33
|
+
if (!editableFocused) return false;
|
|
34
|
+
const visible = Number(visibleHeight) || 0;
|
|
35
|
+
const tallest = Number(tallestHeight) || 0;
|
|
36
|
+
if (!(visible > 0) || !(tallest > 0)) return false;
|
|
37
|
+
const shrink = tallest - visible;
|
|
38
|
+
return shrink >= Math.max(KEYBOARD_MIN_SHRINK_PX, tallest * KEYBOARD_MIN_SHRINK_RATIO);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a press on the ⌄ button keeps the focus where it is. A mouse raises
|
|
43
|
+
* no keyboard, so the composer keeps it as it always has; a touch (or a pen,
|
|
44
|
+
* or a pointer that does not say) keeps it only while the keyboard is open.
|
|
45
|
+
*/
|
|
46
|
+
export function shouldHoldFocusOnJump({ pointerType = '', keyboardOpen = false } = {}) {
|
|
47
|
+
if (String(pointerType || '') === 'mouse') return true;
|
|
48
|
+
return Boolean(keyboardOpen);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The composer takes the focus at page load on a desktop only. */
|
|
52
|
+
export function shouldAutofocusComposerOnLoad({ coarsePointer = false, phoneLayout = false } = {}) {
|
|
53
|
+
return !coarsePointer && !phoneLayout;
|
|
54
|
+
}
|