@adhdev/daemon-core 0.9.82-rc.116 → 0.9.82-rc.118
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/dist/index.js +143 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +143 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +22 -23
- package/src/commands/router.ts +135 -45
- package/src/providers/approval-utils.ts +12 -5
package/package.json
CHANGED
|
@@ -402,11 +402,13 @@ function buildNativeHistoryFallbackReason(args: {
|
|
|
402
402
|
provider?: ProviderModule;
|
|
403
403
|
nativeSource?: string;
|
|
404
404
|
nativeHistoryCoverage?: string;
|
|
405
|
+
unavailableReason?: string;
|
|
405
406
|
nativeMessageCount: number;
|
|
406
407
|
safeMapping: boolean;
|
|
407
408
|
freshEnough: boolean;
|
|
408
409
|
}): string {
|
|
409
410
|
if (!supportsCliNativeTranscript(args.providerType, args.provider)) return 'provider_native_transcript_not_supported';
|
|
411
|
+
if (args.unavailableReason) return `native_history_unavailable:${args.unavailableReason}`;
|
|
410
412
|
if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
|
|
411
413
|
if (args.nativeHistoryCoverage === 'partial') return 'native_history_partial';
|
|
412
414
|
if (args.nativeHistoryCoverage === 'unavailable') return 'native_history_unavailable';
|
|
@@ -509,6 +511,15 @@ function readCliProviderNativeHistory(agentStr: string, args: {
|
|
|
509
511
|
scripts?: ProviderScripts;
|
|
510
512
|
exactSessionScoped?: boolean;
|
|
511
513
|
}): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
|
|
514
|
+
if (!args.historySessionId) {
|
|
515
|
+
return {
|
|
516
|
+
messages: [],
|
|
517
|
+
hasMore: false,
|
|
518
|
+
source: 'native-unavailable',
|
|
519
|
+
unavailableReason: 'native_history_workspace_only_lookup_unsafe',
|
|
520
|
+
lookup: 'session',
|
|
521
|
+
} as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
|
|
522
|
+
}
|
|
512
523
|
const sessionHistory = readProviderChatHistory(agentStr, {
|
|
513
524
|
canonicalHistory: args.canonicalHistory,
|
|
514
525
|
historySessionId: args.historySessionId,
|
|
@@ -519,24 +530,10 @@ function readCliProviderNativeHistory(agentStr: string, args: {
|
|
|
519
530
|
historyBehavior: args.historyBehavior,
|
|
520
531
|
scripts: args.scripts as any,
|
|
521
532
|
});
|
|
522
|
-
//
|
|
523
|
-
// workspace
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
if ((sessionHistory as any).source !== 'native-unavailable' || args.exactSessionScoped || !args.historySessionId || !args.workspace) {
|
|
527
|
-
return { ...(sessionHistory as any), lookup: args.historySessionId ? 'session' : 'workspace' };
|
|
528
|
-
}
|
|
529
|
-
const workspaceHistory = readProviderChatHistory(agentStr, {
|
|
530
|
-
canonicalHistory: args.canonicalHistory,
|
|
531
|
-
historySessionId: undefined,
|
|
532
|
-
workspace: args.workspace,
|
|
533
|
-
offset: args.offset,
|
|
534
|
-
limit: args.limit,
|
|
535
|
-
excludeRecentCount: args.excludeRecentCount,
|
|
536
|
-
historyBehavior: args.historyBehavior,
|
|
537
|
-
scripts: args.scripts as any,
|
|
538
|
-
});
|
|
539
|
-
return { ...(workspaceHistory as any), lookup: 'workspace' };
|
|
533
|
+
// Native transcripts are keyed by provider/runtime session identity. Falling
|
|
534
|
+
// back to workspace makes concurrent local Codex/Hermes sessions alias each
|
|
535
|
+
// other when they share the same cwd.
|
|
536
|
+
return { ...(sessionHistory as any), lookup: 'session' };
|
|
540
537
|
}
|
|
541
538
|
|
|
542
539
|
function isNativeHistoryFreshEnough(args: {
|
|
@@ -1446,11 +1443,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1446
1443
|
const fallbackReason = buildNativeHistoryFallbackReason({
|
|
1447
1444
|
providerType,
|
|
1448
1445
|
provider,
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1446
|
+
nativeSource: (nativeHistory as any).source,
|
|
1447
|
+
nativeHistoryCoverage,
|
|
1448
|
+
unavailableReason,
|
|
1449
|
+
nativeMessageCount: nativeMessages.length,
|
|
1450
|
+
safeMapping,
|
|
1451
|
+
freshEnough,
|
|
1454
1452
|
});
|
|
1455
1453
|
messageSource = buildCliMessageSourceProvenance({
|
|
1456
1454
|
selected: 'pty-parser',
|
|
@@ -1581,6 +1579,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1581
1579
|
provider,
|
|
1582
1580
|
nativeSource: (history as any).source,
|
|
1583
1581
|
nativeHistoryCoverage,
|
|
1582
|
+
unavailableReason,
|
|
1584
1583
|
nativeMessageCount: historyMessages.length,
|
|
1585
1584
|
safeMapping,
|
|
1586
1585
|
freshEnough: true,
|
package/src/commands/router.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import {
|
|
56
56
|
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
57
57
|
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
58
|
+
loadMeshWorktreeBootstrapConfig,
|
|
58
59
|
runMeshWorktreeBootstrap,
|
|
59
60
|
type WorktreeBootstrapState,
|
|
60
61
|
} from '../mesh/worktree-bootstrap-config.js';
|
|
@@ -934,6 +935,12 @@ function finalizeMeshNodeStatus(args: {
|
|
|
934
935
|
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
935
936
|
return;
|
|
936
937
|
}
|
|
938
|
+
if (bootstrap.status === 'running' && bootstrap.required !== false) {
|
|
939
|
+
status.launchReady = false;
|
|
940
|
+
status.launchBlockedReason = 'worktree_bootstrap_running';
|
|
941
|
+
status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
937
944
|
}
|
|
938
945
|
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
939
946
|
status.launchReady = !!daemonId && (
|
|
@@ -1072,9 +1079,12 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
|
1072
1079
|
};
|
|
1073
1080
|
}
|
|
1074
1081
|
|
|
1075
|
-
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
1082
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
|
|
1076
1083
|
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1077
1084
|
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1085
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1086
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1087
|
+
if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
|
|
1078
1088
|
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1079
1089
|
return !recordMeshId || recordMeshId === meshId;
|
|
1080
1090
|
}
|
|
@@ -1123,9 +1133,15 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
1123
1133
|
liveSessionRecords: any[];
|
|
1124
1134
|
allowCoordinatorSession?: boolean;
|
|
1125
1135
|
}): any[] {
|
|
1136
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
1137
|
+
const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
|
|
1138
|
+
&& !!nodeWorkspace
|
|
1139
|
+
&& !fs.existsSync(nodeWorkspace);
|
|
1126
1140
|
const matches = args.liveSessionRecords.filter((record) => {
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1141
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1142
|
+
if (recordNodeId && recordNodeId !== args.nodeId) return false;
|
|
1143
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
|
|
1144
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1129
1145
|
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
1130
1146
|
});
|
|
1131
1147
|
|
|
@@ -1148,11 +1164,15 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1148
1164
|
}): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
|
|
1149
1165
|
const liveNodeIds = new Set<string>();
|
|
1150
1166
|
const liveWorkspaces = new Set<string>();
|
|
1167
|
+
const missingLocalWorktreeNodeIds = new Set<string>();
|
|
1151
1168
|
for (const node of args.nodes || []) {
|
|
1152
1169
|
const nodeId = readStringValue(node?.id, node?.nodeId);
|
|
1153
1170
|
const workspace = readStringValue(node?.workspace);
|
|
1154
1171
|
if (nodeId) liveNodeIds.add(nodeId);
|
|
1155
1172
|
if (workspace) liveWorkspaces.add(workspace);
|
|
1173
|
+
if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
|
|
1174
|
+
missingLocalWorktreeNodeIds.add(nodeId);
|
|
1175
|
+
}
|
|
1156
1176
|
}
|
|
1157
1177
|
|
|
1158
1178
|
const sessions: Record<string, unknown>[] = [];
|
|
@@ -1162,7 +1182,7 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1162
1182
|
if (recordMeshId !== args.meshId) continue;
|
|
1163
1183
|
const recordNodeId = readStringValue(meta.meshNodeId);
|
|
1164
1184
|
const workspace = readStringValue(record?.workspace);
|
|
1165
|
-
const removedNode = !!recordNodeId && !liveNodeIds.has(recordNodeId);
|
|
1185
|
+
const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
|
|
1166
1186
|
const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
|
|
1167
1187
|
if (!removedNode && !orphanedWorkspace) continue;
|
|
1168
1188
|
sessions.push({
|
|
@@ -4971,60 +4991,120 @@ export class DaemonCommandRouter {
|
|
|
4971
4991
|
this.invalidateAggregateMeshStatus(meshId);
|
|
4972
4992
|
}
|
|
4973
4993
|
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
await runGit(
|
|
4980
|
-
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
4981
|
-
['submodule', 'update', '--init', '--recursive'],
|
|
4982
|
-
{ timeoutMs: 120000 },
|
|
4983
|
-
);
|
|
4984
|
-
} catch (subErr: any) {
|
|
4985
|
-
// Submodule init is best-effort; don't fail the clone
|
|
4986
|
-
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
4994
|
+
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
4995
|
+
node.worktreeBootstrap = bootstrapState;
|
|
4996
|
+
if (meshRecord.inline) {
|
|
4997
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
4998
|
+
return;
|
|
4987
4999
|
}
|
|
4988
|
-
}
|
|
4989
|
-
|
|
4990
|
-
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
4991
|
-
node.worktreeBootstrap = bootstrapState;
|
|
4992
|
-
if (!meshRecord.inline) {
|
|
4993
5000
|
try {
|
|
4994
5001
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
4995
5002
|
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
4996
5003
|
this.invalidateAggregateMeshStatus(meshId);
|
|
4997
5004
|
} catch { /* bootstrap status persistence is best-effort */ }
|
|
4998
|
-
}
|
|
5005
|
+
};
|
|
4999
5006
|
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5007
|
+
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5008
|
+
try {
|
|
5009
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
5010
|
+
appendLedgerEntry(meshId, {
|
|
5011
|
+
kind: 'node_cloned',
|
|
5012
|
+
nodeId: node.id,
|
|
5013
|
+
payload: {
|
|
5014
|
+
sourceNodeId,
|
|
5015
|
+
branch: result.branch,
|
|
5016
|
+
worktreePath: result.worktreePath,
|
|
5017
|
+
submodulesInitialized: initSubmodules,
|
|
5018
|
+
worktreeBootstrap: {
|
|
5019
|
+
status: bootstrapState.status,
|
|
5020
|
+
required: bootstrapState.required,
|
|
5021
|
+
configSource: bootstrapState.configSource,
|
|
5022
|
+
configSourceType: bootstrapState.configSourceType,
|
|
5023
|
+
lastCommand: bootstrapState.lastCommand,
|
|
5024
|
+
exitCode: bootstrapState.exitCode,
|
|
5025
|
+
},
|
|
5018
5026
|
},
|
|
5019
|
-
}
|
|
5027
|
+
});
|
|
5028
|
+
} catch { /* ledger append is best-effort */ }
|
|
5029
|
+
};
|
|
5030
|
+
|
|
5031
|
+
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
5032
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
5033
|
+
const runningBootstrapState: WorktreeBootstrapState = {
|
|
5034
|
+
status: 'running',
|
|
5035
|
+
required: loadedBootstrap.config?.required !== false,
|
|
5036
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
5037
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
5038
|
+
startedAt: new Date().toISOString(),
|
|
5039
|
+
};
|
|
5040
|
+
await persistWorktreeSetupState(runningBootstrapState);
|
|
5041
|
+
|
|
5042
|
+
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
5043
|
+
let submodulesInitialized = false;
|
|
5044
|
+
if (initSubmodules) {
|
|
5045
|
+
try {
|
|
5046
|
+
const { runGit } = await import('../git/git-executor.js');
|
|
5047
|
+
await runGit(
|
|
5048
|
+
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
5049
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
5050
|
+
{ timeoutMs: 120000 },
|
|
5051
|
+
);
|
|
5052
|
+
submodulesInitialized = true;
|
|
5053
|
+
} catch (subErr: any) {
|
|
5054
|
+
// Submodule init is best-effort; don't fail the clone
|
|
5055
|
+
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
5059
|
+
await persistWorktreeSetupState(bootstrapState);
|
|
5060
|
+
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
5061
|
+
return { submodulesInitialized, bootstrapState };
|
|
5062
|
+
};
|
|
5063
|
+
|
|
5064
|
+
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
5065
|
+
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
5066
|
+
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
5067
|
+
: 8000;
|
|
5068
|
+
const setupPromise = finishWorktreeSetup();
|
|
5069
|
+
const setupResult = await Promise.race([
|
|
5070
|
+
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
5071
|
+
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
5072
|
+
]);
|
|
5073
|
+
|
|
5074
|
+
if (!setupResult.completed) {
|
|
5075
|
+
setupPromise.catch((error: any) => {
|
|
5076
|
+
const failedState: WorktreeBootstrapState = {
|
|
5077
|
+
...runningBootstrapState,
|
|
5078
|
+
status: 'failed',
|
|
5079
|
+
completedAt: new Date().toISOString(),
|
|
5080
|
+
error: error?.message || String(error),
|
|
5081
|
+
};
|
|
5082
|
+
void persistWorktreeSetupState(failedState);
|
|
5083
|
+
void appendCloneLedger(false, failedState);
|
|
5020
5084
|
});
|
|
5021
|
-
|
|
5085
|
+
return {
|
|
5086
|
+
success: true,
|
|
5087
|
+
async: true,
|
|
5088
|
+
status: 'accepted',
|
|
5089
|
+
node,
|
|
5090
|
+
worktreePath: result.worktreePath,
|
|
5091
|
+
branch: result.branch,
|
|
5092
|
+
worktreeBootstrap: runningBootstrapState,
|
|
5093
|
+
worktreeSetup: {
|
|
5094
|
+
status: 'running',
|
|
5095
|
+
setupWaitMs,
|
|
5096
|
+
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
5097
|
+
},
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5022
5100
|
|
|
5101
|
+
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
5023
5102
|
return {
|
|
5024
5103
|
success: true,
|
|
5025
5104
|
node,
|
|
5026
5105
|
worktreePath: result.worktreePath,
|
|
5027
5106
|
branch: result.branch,
|
|
5107
|
+
submodulesInitialized,
|
|
5028
5108
|
worktreeBootstrap: bootstrapState,
|
|
5029
5109
|
};
|
|
5030
5110
|
} catch (e: any) {
|
|
@@ -5569,12 +5649,22 @@ export class DaemonCommandRouter {
|
|
|
5569
5649
|
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
5570
5650
|
const nodeId = String(node.id || node.nodeId || '');
|
|
5571
5651
|
const daemonId = readStringValue(node.daemonId);
|
|
5652
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
5653
|
+
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
5572
5654
|
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
5655
|
+
const configuredCoordinatorNode = Boolean(
|
|
5656
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
5657
|
+
);
|
|
5658
|
+
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
5659
|
+
&& !daemonId
|
|
5660
|
+
&& !nodeMachineId
|
|
5661
|
+
&& !nodeHostname;
|
|
5573
5662
|
const isSelfNode = Boolean(
|
|
5574
5663
|
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
5575
5664
|
) || Boolean(
|
|
5576
5665
|
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
5577
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5666
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5667
|
+
|| sparseConfiguredCoordinatorNode;
|
|
5578
5668
|
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
5579
5669
|
localMachineId,
|
|
5580
5670
|
localDaemonId: this.deps.statusInstanceId,
|
|
@@ -5593,7 +5683,7 @@ export class DaemonCommandRouter {
|
|
|
5593
5683
|
worktreeBranch: node.worktreeBranch,
|
|
5594
5684
|
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
5595
5685
|
daemonId,
|
|
5596
|
-
machineId:
|
|
5686
|
+
machineId: nodeMachineId || node.machineId,
|
|
5597
5687
|
machine: machineIdentity,
|
|
5598
5688
|
machineStatus: node.machineStatus,
|
|
5599
5689
|
health: 'unknown',
|
|
@@ -24,6 +24,13 @@ function normalizeApprovalLabel(value: string): string {
|
|
|
24
24
|
.trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isNegativeApprovalLabel(value: string): boolean {
|
|
28
|
+
const label = normalizeApprovalLabel(value);
|
|
29
|
+
return /^(no|deny|reject|cancel|skip|exit|stop)\b/.test(label)
|
|
30
|
+
|| /\bwithout\b/.test(label)
|
|
31
|
+
|| /\bdo not\b/.test(label);
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[] {
|
|
28
35
|
const customHints = Array.isArray(provider?.approvalPositiveHints)
|
|
29
36
|
? provider.approvalPositiveHints
|
|
@@ -39,24 +46,24 @@ export function pickApprovalButton(
|
|
|
39
46
|
): { index: number; label: string } {
|
|
40
47
|
const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
|
|
41
48
|
if (labels.length === 0) {
|
|
42
|
-
return { index:
|
|
49
|
+
return { index: -1, label: '' };
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label));
|
|
46
53
|
const hints = getApprovalPositiveHints(provider);
|
|
47
54
|
|
|
48
55
|
for (const hint of hints) {
|
|
49
|
-
const exactIndex = normalizedButtons.findIndex((label) => label === hint);
|
|
56
|
+
const exactIndex = normalizedButtons.findIndex((label, index) => label === hint && !isNegativeApprovalLabel(labels[index]));
|
|
50
57
|
if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] };
|
|
51
58
|
|
|
52
|
-
const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint));
|
|
59
|
+
const prefixIndex = normalizedButtons.findIndex((label, index) => label.startsWith(hint) && !isNegativeApprovalLabel(labels[index]));
|
|
53
60
|
if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] };
|
|
54
61
|
|
|
55
|
-
const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint));
|
|
62
|
+
const includeIndex = normalizedButtons.findIndex((label, index) => label.includes(hint) && !isNegativeApprovalLabel(labels[index]));
|
|
56
63
|
if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] };
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
return { index:
|
|
66
|
+
return { index: -1, label: '' };
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
|