macOS: show friendly device names in Instances
parent
09819f8b2e
commit
7950a646c3
|
|
@ -0,0 +1,69 @@
|
|||
import Foundation
|
||||
|
||||
struct DevicePresentation: Sendable {
|
||||
let title: String
|
||||
let symbol: String?
|
||||
}
|
||||
|
||||
enum DeviceModelCatalog {
|
||||
static func presentation(deviceFamily: String?, modelIdentifier: String?) -> DevicePresentation? {
|
||||
let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
let modelEntry = model.isEmpty ? nil : modelIdentifierTable[model]
|
||||
let symbol = modelEntry?.symbol ?? fallbackSymbol(for: family, modelIdentifier: model)
|
||||
|
||||
let title = if let name = modelEntry?.name, !name.isEmpty {
|
||||
name
|
||||
} else if !family.isEmpty, !model.isEmpty {
|
||||
"\(family) (\(model))"
|
||||
} else if !family.isEmpty {
|
||||
family
|
||||
} else if !model.isEmpty {
|
||||
model
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if title.isEmpty { return nil }
|
||||
return DevicePresentation(title: title, symbol: symbol)
|
||||
}
|
||||
|
||||
private static func fallbackSymbol(for familyRaw: String, modelIdentifier: String) -> String? {
|
||||
let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if family.isEmpty { return nil }
|
||||
switch family.lowercased() {
|
||||
case "ipad":
|
||||
return "ipad"
|
||||
case "iphone":
|
||||
return "iphone"
|
||||
case "mac":
|
||||
return "laptopcomputer"
|
||||
case "android":
|
||||
// Prefer tablet glyph when we know it's an Android tablet. (No attempt to infer phone/tablet here.)
|
||||
return "cpu"
|
||||
case "linux":
|
||||
return "cpu"
|
||||
default:
|
||||
return "cpu"
|
||||
}
|
||||
}
|
||||
|
||||
private struct ModelEntry: Sendable {
|
||||
let name: String
|
||||
let symbol: String?
|
||||
}
|
||||
|
||||
// Friendly model names for a small set of known identifiers.
|
||||
// Extend this table as needed; unknown identifiers fall back to the raw value.
|
||||
private static let modelIdentifierTable: [String: ModelEntry] = [
|
||||
// iPad
|
||||
"iPad16,5": .init(name: "iPad Pro 11-inch (M4)", symbol: "ipad"),
|
||||
"iPad16,6": .init(name: "iPad Pro 13-inch (M4)", symbol: "ipad"),
|
||||
|
||||
// Mac
|
||||
"Mac16,6": .init(name: "MacBook Pro (14-inch, 2024)", symbol: "laptopcomputer"),
|
||||
"Mac16,8": .init(name: "MacBook Pro (16-inch, 2024)", symbol: "laptopcomputer"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +70,11 @@ struct InstancesSettings: View {
|
|||
if let platform = inst.platform, let prettyPlatform = self.prettyPlatform(platform) {
|
||||
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
|
||||
}
|
||||
if let deviceText = self.deviceDescription(inst),
|
||||
let deviceIcon = self.deviceIcon(inst)
|
||||
if let device = DeviceModelCatalog.presentation(
|
||||
deviceFamily: inst.deviceFamily,
|
||||
modelIdentifier: inst.modelIdentifier)
|
||||
{
|
||||
self.label(icon: deviceIcon, text: deviceText)
|
||||
self.label(icon: device.symbol, text: device.title)
|
||||
}
|
||||
self.label(icon: "clock", text: inst.lastInputDescription)
|
||||
if let mode = inst.mode { self.label(icon: "network", text: mode) }
|
||||
|
|
@ -94,9 +95,11 @@ struct InstancesSettings: View {
|
|||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
private func label(icon: String, text: String) -> some View {
|
||||
private func label(icon: String?, text: String) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
if let icon {
|
||||
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
.font(.footnote)
|
||||
|
|
@ -120,28 +123,6 @@ struct InstancesSettings: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func deviceIcon(_ inst: InstanceInfo) -> String? {
|
||||
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if family.isEmpty { return nil }
|
||||
switch family.lowercased() {
|
||||
case "ipad":
|
||||
return "ipad"
|
||||
case "iphone":
|
||||
return "iphone"
|
||||
case "mac":
|
||||
return "laptopcomputer"
|
||||
default:
|
||||
return "cpu"
|
||||
}
|
||||
}
|
||||
|
||||
private func deviceDescription(_ inst: InstanceInfo) -> String? {
|
||||
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let model = inst.modelIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !family.isEmpty, !model.isEmpty { return "\(family) (\(model))" }
|
||||
if !model.isEmpty { return model }
|
||||
return family.isEmpty ? nil : family
|
||||
}
|
||||
private func prettyPlatform(_ raw: String) -> String? {
|
||||
let (prefix, version) = self.parsePlatform(raw)
|
||||
if prefix.isEmpty { return nil }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import { codingTools, readTool } from "@mariozechner/pi-coding-agent";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
|
||||
import { detectMime } from "../media/mime.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -949,7 +949,10 @@ export async function startGatewayServer(
|
|||
if (!key) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code: ErrorCodes.INVALID_REQUEST, message: "key required" },
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "key required",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1102,7 +1105,10 @@ export async function startGatewayServer(
|
|||
chatAbortControllers.delete(runId);
|
||||
chatRunBuffers.delete(runId);
|
||||
const current = chatRunSessions.get(active.sessionId);
|
||||
if (current?.clientRunId === runId && current.sessionKey === sessionKey) {
|
||||
if (
|
||||
current?.clientRunId === runId &&
|
||||
current.sessionKey === sessionKey
|
||||
) {
|
||||
chatRunSessions.delete(active.sessionId);
|
||||
}
|
||||
|
||||
|
|
@ -2126,10 +2132,13 @@ export async function startGatewayServer(
|
|||
: [];
|
||||
const hardMax = 1000;
|
||||
const defaultLimit = 200;
|
||||
const requested = typeof limit === "number" ? limit : defaultLimit;
|
||||
const requested =
|
||||
typeof limit === "number" ? limit : defaultLimit;
|
||||
const max = Math.min(hardMax, requested);
|
||||
const sliced =
|
||||
rawMessages.length > max ? rawMessages.slice(-max) : rawMessages;
|
||||
rawMessages.length > max
|
||||
? rawMessages.slice(-max)
|
||||
: rawMessages;
|
||||
const capped = capArrayByJsonBytes(
|
||||
sliced,
|
||||
MAX_CHAT_HISTORY_MESSAGES_BYTES,
|
||||
|
|
@ -2268,7 +2277,9 @@ export async function startGatewayServer(
|
|||
break;
|
||||
}
|
||||
}
|
||||
const { storePath, store, entry } = loadSessionEntry(p.sessionKey);
|
||||
const { storePath, store, entry } = loadSessionEntry(
|
||||
p.sessionKey,
|
||||
);
|
||||
const now = Date.now();
|
||||
const sessionId = entry?.sessionId ?? randomUUID();
|
||||
const sessionEntry: SessionEntry = {
|
||||
|
|
@ -2441,7 +2452,10 @@ export async function startGatewayServer(
|
|||
);
|
||||
break;
|
||||
}
|
||||
const p = params as { id: string; patch: Record<string, unknown> };
|
||||
const p = params as {
|
||||
id: string;
|
||||
patch: Record<string, unknown>;
|
||||
};
|
||||
const job = await cron.update(
|
||||
p.id,
|
||||
p.patch as unknown as CronJobPatch,
|
||||
|
|
@ -2683,7 +2697,9 @@ export async function startGatewayServer(
|
|||
const version =
|
||||
typeof params.version === "string" ? params.version : undefined;
|
||||
const platform =
|
||||
typeof params.platform === "string" ? params.platform : undefined;
|
||||
typeof params.platform === "string"
|
||||
? params.platform
|
||||
: undefined;
|
||||
const deviceFamily =
|
||||
typeof params.deviceFamily === "string"
|
||||
? params.deviceFamily
|
||||
|
|
@ -2725,7 +2741,8 @@ export async function startGatewayServer(
|
|||
normalizedReason === "heartbeat";
|
||||
if (!(isNodePresenceLine && looksPeriodic)) {
|
||||
const compactNodeText =
|
||||
isNodePresenceLine && (host || ip || version || mode || reason)
|
||||
isNodePresenceLine &&
|
||||
(host || ip || version || mode || reason)
|
||||
? `Node: ${host?.trim() || "Unknown"}${ip ? ` (${ip})` : ""} · app ${version?.trim() || "unknown"} · mode ${mode?.trim() || "unknown"} · reason ${reason?.trim() || "event"}`
|
||||
: text;
|
||||
enqueueSystemEvent(compactNodeText);
|
||||
|
|
@ -3137,7 +3154,11 @@ export async function startGatewayServer(
|
|||
}
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error });
|
||||
dedupe.set(`send:${idem}`, {
|
||||
ts: Date.now(),
|
||||
ok: false,
|
||||
error,
|
||||
});
|
||||
respond(false, undefined, error, {
|
||||
provider,
|
||||
error: formatForLog(err),
|
||||
|
|
@ -3180,7 +3201,8 @@ export async function startGatewayServer(
|
|||
const message = params.message.trim();
|
||||
|
||||
const requestedSessionKey =
|
||||
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
||||
typeof params.sessionKey === "string" &&
|
||||
params.sessionKey.trim()
|
||||
? params.sessionKey.trim()
|
||||
: undefined;
|
||||
let resolvedSessionId = params.sessionId?.trim() || undefined;
|
||||
|
|
|
|||
Loading…
Reference in New Issue